|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
//
// Description: TextRange provider adaptor for Text Object Model based objects
//
using System.Collections; // Hashtable
using System.Collections.ObjectModel; // ReadOnlyCollection<T>
using System.Globalization; // CultureInfo
using System.Windows; // TextDecorationCollection
using System.Windows.Automation; // TextPatternIdentifiers
using System.Windows.Automation.Peers; // AutomationPeer
using System.Windows.Automation.Provider; // ITextRangeProvider
using System.Windows.Automation.Text; // TextUnit
using System.Windows.Markup; // XmlLanguage
using System.Windows.Media; // FontFamily, Brush
using System.Windows.Documents; // ITextPointer
using MS.Internal.Documents; // TextContainerHelper
namespace MS.Internal.Automation
{
/// <summary>
/// Implements the UIA's ITextRangeProvider interface for WCP text providing controls
/// </summary>
internal class TextRangeAdaptor : ITextRangeProvider
{
//-------------------------------------------------------------------
//
// Constructors
//
//-------------------------------------------------------------------
#region Constructors
/// <summary>
/// Static initialization.
/// </summary>
static TextRangeAdaptor()
{
_textPatternAttributes = new Hashtable();
// AnimationStyle
_textPatternAttributes.Add(
TextPatternIdentifiers.AnimationStyleAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
TextEffectCollection effects = tp.GetValue(TextElement.TextEffectsProperty) as TextEffectCollection;
return (effects != null && effects.Count > 0) ? AnimationStyle.Other : AnimationStyle.None;
},
delegate(object val1, object val2) { return (AnimationStyle)val1 == (AnimationStyle)val2; })
);
// BackgroundColor
_textPatternAttributes.Add(
TextPatternIdentifiers.BackgroundColorAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
return ColorFromBrush(tp.GetValue(TextElement.BackgroundProperty));
},
delegate(object val1, object val2) { return (int)val1 == (int)val2; })
);
// BulletStyle
_textPatternAttributes.Add(
TextPatternIdentifiers.BulletStyleAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
object val = tp.GetValue(List.MarkerStyleProperty);
if (val is TextMarkerStyle)
{
switch ((TextMarkerStyle)val)
{
case TextMarkerStyle.None:
val = BulletStyle.None;
break;
case TextMarkerStyle.Disc:
val = BulletStyle.FilledRoundBullet;
break;
case TextMarkerStyle.Circle:
val = BulletStyle.HollowRoundBullet;
break;
case TextMarkerStyle.Square:
val = BulletStyle.HollowSquareBullet;
break;
case TextMarkerStyle.Box:
val = BulletStyle.FilledSquareBullet;
break;
default:
val = BulletStyle.Other;
break;
}
}
else
{
val = BulletStyle.None;
}
return val;
},
delegate(object val1, object val2) { return (BulletStyle)val1 == (BulletStyle)val2; })
);
// CapStyle
_textPatternAttributes.Add(
TextPatternIdentifiers.CapStyleAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
FontCapitals capsWCP = (FontCapitals)tp.GetValue(Typography.CapitalsProperty);
CapStyle capsUIA;
switch (capsWCP)
{
case FontCapitals.Normal:
capsUIA = CapStyle.None;
break;
case FontCapitals.AllSmallCaps:
capsUIA = CapStyle.AllCap;
break;
case FontCapitals.SmallCaps:
capsUIA = CapStyle.SmallCap;
break;
case FontCapitals.AllPetiteCaps:
capsUIA = CapStyle.AllPetiteCaps;
break;
case FontCapitals.PetiteCaps:
capsUIA = CapStyle.PetiteCaps;
break;
case FontCapitals.Unicase:
capsUIA = CapStyle.Unicase;
break;
case FontCapitals.Titling:
capsUIA = CapStyle.Titling;
break;
default:
capsUIA = CapStyle.Other;
break;
}
return capsUIA;
},
delegate(object val1, object val2) { return (CapStyle)val1 == (CapStyle)val2; })
);
// Culture
_textPatternAttributes.Add(
TextPatternIdentifiers.CultureAttribute,
new TextAttributeHelper(
// UIAutomation expects an LCID on the provider side
delegate(ITextPointer tp)
{
object val = tp.GetValue(FrameworkElement.LanguageProperty);
return (val is XmlLanguage) ? ((XmlLanguage)val).GetEquivalentCulture().LCID : CultureInfo.InvariantCulture.LCID;
},
delegate(object val1, object val2) { return (int)val1 == (int)val2; })
);
// FontName
_textPatternAttributes.Add(
TextPatternIdentifiers.FontNameAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
return GetFontFamilyName((FontFamily)tp.GetValue(TextElement.FontFamilyProperty), tp);
},
delegate(object val1, object val2) { return (val1 as string) == (val2 as string); })
);
// FontSize
_textPatternAttributes.Add(
TextPatternIdentifiers.FontSizeAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
return NativeObjectLengthToPoints((double)tp.GetValue(TextElement.FontSizeProperty));
},
delegate(object val1, object val2) { return (double)val1 == (double)val2; })
);
// FontWeight
_textPatternAttributes.Add(
TextPatternIdentifiers.FontWeightAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
FontWeight fontWeight = (FontWeight)tp.GetValue(TextElement.FontWeightProperty);
return fontWeight.ToOpenTypeWeight();
},
delegate(object val1, object val2) { return (int)val1 == (int)val2; })
);
// ForegroundColor
_textPatternAttributes.Add(
TextPatternIdentifiers.ForegroundColorAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
return ColorFromBrush(tp.GetValue(TextElement.ForegroundProperty));
},
delegate(object val1, object val2) { return (int)val1 == (int)val2; })
);
// HorizontalTextAlignment
_textPatternAttributes.Add(
TextPatternIdentifiers.HorizontalTextAlignmentAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
TextAlignment alignmentWCP = (TextAlignment)tp.GetValue(Block.TextAlignmentProperty);
HorizontalTextAlignment alignmentUIA;
switch (alignmentWCP)
{
case TextAlignment.Left:
default:
alignmentUIA = HorizontalTextAlignment.Left;
break;
case TextAlignment.Right:
alignmentUIA = HorizontalTextAlignment.Right;
break;
case TextAlignment.Center:
alignmentUIA = HorizontalTextAlignment.Centered;
break;
case TextAlignment.Justify:
alignmentUIA = HorizontalTextAlignment.Justified;
break;
}
return alignmentUIA;
},
delegate(object val1, object val2) { return (HorizontalTextAlignment)val1 == (HorizontalTextAlignment)val2; })
);
// IndentationFirstLine
_textPatternAttributes.Add(
TextPatternIdentifiers.IndentationFirstLineAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
return NativeObjectLengthToPoints((double)tp.GetValue(Paragraph.TextIndentProperty));
},
delegate(object val1, object val2) { return (double)val1 == (double)val2; })
);
// IndentationLeading
_textPatternAttributes.Add(
TextPatternIdentifiers.IndentationLeadingAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
Thickness padding = (Thickness)tp.GetValue(Block.PaddingProperty);
return padding.IsValid(true, false, false, false) ? NativeObjectLengthToPoints(padding.Left) : 0;
},
delegate(object val1, object val2) { return (double)val1 == (double)val2; })
);
// IndentationTrailing
_textPatternAttributes.Add(
TextPatternIdentifiers.IndentationTrailingAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
Thickness padding = (Thickness)tp.GetValue(Block.PaddingProperty);
return padding.IsValid(true, false, false, false) ? NativeObjectLengthToPoints(padding.Right) : 0;
},
delegate(object val1, object val2) { return (double)val1 == (double)val2; })
);
// IsHidden
_textPatternAttributes.Add(
TextPatternIdentifiers.IsHiddenAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp) { return false; },
delegate(object val1, object val2) { return (bool)val1 == (bool)val2; })
);
// IsItalic
_textPatternAttributes.Add(
TextPatternIdentifiers.IsItalicAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
FontStyle style = (FontStyle)tp.GetValue(TextElement.FontStyleProperty);
// FontStyles.Oblique is assumed a sort of Italic (#1053181).
return (style == FontStyles.Italic || style == FontStyles.Oblique);
},
delegate(object val1, object val2) { return (bool)val1 == (bool)val2; })
);
// IsReadOnly
_textPatternAttributes.Add(
TextPatternIdentifiers.IsReadOnlyAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
bool readOnly = false;
if (tp.TextContainer.TextSelection != null)
{
readOnly = tp.TextContainer.TextSelection.TextEditor.IsReadOnly;
}
return readOnly;
},
delegate(object val1, object val2) { return (bool)val1 == (bool)val2; })
);
// IsSubscript
_textPatternAttributes.Add(
TextPatternIdentifiers.IsSubscriptAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
FontVariants fontVariants = (FontVariants)tp.GetValue(Typography.VariantsProperty);
return (fontVariants == FontVariants.Subscript);
},
delegate(object val1, object val2) { return (bool)val1 == (bool)val2; })
);
// IsSuperscript
_textPatternAttributes.Add(
TextPatternIdentifiers.IsSuperscriptAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
FontVariants fontVariants = (FontVariants)tp.GetValue(Typography.VariantsProperty);
return (fontVariants == FontVariants.Superscript);
},
delegate(object val1, object val2) { return (bool)val1 == (bool)val2; })
);
// MarginBottom
_textPatternAttributes.Add(
TextPatternIdentifiers.MarginBottomAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
Thickness margin = (Thickness)tp.GetValue(FrameworkElement.MarginProperty);
return margin.IsValid(true, false, false, false) ? NativeObjectLengthToPoints(margin.Bottom) : 0;
},
delegate(object val1, object val2) { return (double)val1 == (double)val2; })
);
// MarginLeading
_textPatternAttributes.Add(
TextPatternIdentifiers.MarginLeadingAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
Thickness margin = (Thickness)tp.GetValue(FrameworkElement.MarginProperty);
return margin.IsValid(true, false, false, false) ? NativeObjectLengthToPoints(margin.Left) : 0;
},
delegate(object val1, object val2) { return (double)val1 == (double)val2; })
);
// MarginTop
_textPatternAttributes.Add(
TextPatternIdentifiers.MarginTopAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
Thickness margin = (Thickness)tp.GetValue(FrameworkElement.MarginProperty);
return margin.IsValid(true, false, false, false) ? NativeObjectLengthToPoints(margin.Top) : 0;
},
delegate(object val1, object val2) { return (double)val1 == (double)val2; })
);
// MarginTrailing
_textPatternAttributes.Add(
TextPatternIdentifiers.MarginTrailingAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
Thickness margin = (Thickness)tp.GetValue(FrameworkElement.MarginProperty);
return margin.IsValid(true, false, false, false) ? NativeObjectLengthToPoints(margin.Right) : 0;
},
delegate(object val1, object val2) { return (double)val1 == (double)val2; })
);
// OutlineStyles
_textPatternAttributes.Add(
TextPatternIdentifiers.OutlineStylesAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp) { return OutlineStyles.None; },
delegate(object val1, object val2) { return (OutlineStyles)val1 == (OutlineStyles)val2; })
);
// OverlineColor
_textPatternAttributes.Add(
TextPatternIdentifiers.OverlineColorAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
object decors = tp.GetValue(Inline.TextDecorationsProperty);
return GetTextDecorationColor(decors as TextDecorationCollection, TextDecorationLocation.OverLine);
},
delegate(object val1, object val2) { return (int)val1 == (int)val2; })
);
// OverlineStyle
_textPatternAttributes.Add(
TextPatternIdentifiers.OverlineStyleAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
object decors = tp.GetValue(Inline.TextDecorationsProperty);
return GetTextDecorationLineStyle(decors as TextDecorationCollection, TextDecorationLocation.OverLine);
},
delegate(object val1, object val2) { return (TextDecorationLineStyle)val1 == (TextDecorationLineStyle)val2; })
);
// StrikeThroughColor
_textPatternAttributes.Add(
TextPatternIdentifiers.StrikethroughColorAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
object decors = tp.GetValue(Inline.TextDecorationsProperty);
return GetTextDecorationColor(decors as TextDecorationCollection, TextDecorationLocation.Strikethrough);
},
delegate(object val1, object val2) { return (int)val1 == (int)val2; })
);
// StrikeThroughStyle
_textPatternAttributes.Add(
TextPatternIdentifiers.StrikethroughStyleAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
object decors = tp.GetValue(Inline.TextDecorationsProperty);
return GetTextDecorationLineStyle(decors as TextDecorationCollection, TextDecorationLocation.Strikethrough);
},
delegate(object val1, object val2) { return (TextDecorationLineStyle)val1 == (TextDecorationLineStyle)val2; })
);
// TextFlowDirections
_textPatternAttributes.Add(
TextPatternIdentifiers.TextFlowDirectionsAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
FlowDirection flowWCP = (FlowDirection)tp.GetValue(FrameworkElement.FlowDirectionProperty);
FlowDirections flowUIA;
switch (flowWCP)
{
case FlowDirection.LeftToRight:
default:
flowUIA = FlowDirections.Default;
break;
case FlowDirection.RightToLeft:
flowUIA = FlowDirections.RightToLeft;
break;
}
return flowUIA;
},
delegate(object val1, object val2) { return (FlowDirections)val1 == (FlowDirections)val2; })
);
// UnderlineColor
_textPatternAttributes.Add(
TextPatternIdentifiers.UnderlineColorAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
object decors = tp.GetValue(Inline.TextDecorationsProperty);
return GetTextDecorationColor(decors as TextDecorationCollection, TextDecorationLocation.Underline);
},
delegate(object val1, object val2) { return (int)val1 == (int)val2; })
);
// UnderlineStyle
_textPatternAttributes.Add(
TextPatternIdentifiers.UnderlineStyleAttribute,
new TextAttributeHelper(
delegate(ITextPointer tp)
{
object decors = tp.GetValue(Inline.TextDecorationsProperty);
return GetTextDecorationLineStyle(decors as TextDecorationCollection, TextDecorationLocation.Underline);
},
delegate(object val1, object val2) { return (TextDecorationLineStyle)val1 == (TextDecorationLineStyle)val2; })
);
// TextPatternIdentifiers.TabsAttribute
}
/// <summary>
/// Constructor
/// </summary>
internal TextRangeAdaptor(TextAdaptor textAdaptor, ITextPointer start, ITextPointer end, AutomationPeer textPeer)
{
Invariant.Assert(textAdaptor != null, "Invalid textAdaptor.");
Invariant.Assert(textPeer != null, "Invalid textPeer.");
Invariant.Assert(start != null && end != null, "Invalid range.");
Invariant.Assert(start.CompareTo(end) <= 0, "Invalid range, end < start.");
_textAdaptor = textAdaptor;
_start = start.CreatePointer();
_end = end.CreatePointer();
_textPeer = textPeer;
}
#endregion Constructors
#region Internal methods
/// <summary>
/// This wrapper is to cover up a shortcoming in MoveToInsertionPosition code.
/// If the position is inside a Hyperlink, it is being moved out of the hyperlink and to the previous
/// insertion position which can be on the previous line (or unit) and this is creating problems
/// This is a temporary solution until we find a better solution to handle the case
/// There is also a related bug in MoveToLineBoundary: PS#1742102
/// </summary>
internal static bool MoveToInsertionPosition(ITextPointer position, LogicalDirection direction)
{
if (!position.TextContainer.IsReadOnly ||
(!TextPointerBase.IsAtNonMergeableInlineStart(position) && !TextPointerBase.IsAtNonMergeableInlineEnd(position)))
{
return position.MoveToInsertionPosition(direction);
}
return false;
}
#endregion Internal methods
//-------------------------------------------------------------------
//
// Private Methods
//
//-------------------------------------------------------------------
#region Private Methods
/// <summary>
/// Verifies that the given range points to the same text container as this one.
/// </summary>
/// <returns>The validated range casted to TextRangeAdaptor</returns>
private TextRangeAdaptor ValidateAndThrow(ITextRangeProvider range)
{
TextRangeAdaptor rangeAdaptor = range as TextRangeAdaptor;
if (rangeAdaptor == null || rangeAdaptor._start.TextContainer != _start.TextContainer)
{
throw new ArgumentException(SR.TextRangeProvider_WrongTextRange);
}
return rangeAdaptor;
}
/// <summary>
/// Expands the range to an integral number of enclosing units. If the range is already an
/// integral number of the specified units then it remains unchanged.
/// </summary>
private void ExpandToEnclosingUnit(TextUnit unit, bool expandStart, bool expandEnd)
{
ITextView textView;
switch (unit)
{
case TextUnit.Character:
if (expandStart && !TextPointerBase.IsAtInsertionPosition(_start))
{
TextPointerBase.MoveToNextInsertionPosition(_start, LogicalDirection.Backward);
}
if (expandEnd && !TextPointerBase.IsAtInsertionPosition(_end))
{
TextPointerBase.MoveToNextInsertionPosition(_end, LogicalDirection.Forward);
}
break;
case TextUnit.Word:
if (expandStart && !IsAtWordBoundary(_start))
{
MoveToNextWordBoundary(_start, LogicalDirection.Backward);
}
if (expandEnd && !IsAtWordBoundary(_end))
{
MoveToNextWordBoundary(_end, LogicalDirection.Forward);
}
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.
if (expandStart)
{
TextPointerContext forwardContext = _start.GetPointerContext(LogicalDirection.Forward);
while (true)
{
TextPointerContext backwardContext = _start.GetPointerContext(LogicalDirection.Backward);
if (backwardContext == TextPointerContext.None)
break;
if (forwardContext == TextPointerContext.Text && backwardContext != TextPointerContext.Text)
break;
forwardContext = backwardContext;
_start.MoveToNextContextPosition(LogicalDirection.Backward);
}
}
if (expandEnd)
{
TextPointerContext backwardContext = _end.GetPointerContext(LogicalDirection.Backward);
while (true)
{
TextPointerContext forwardContext = _end.GetPointerContext(LogicalDirection.Forward);
if (forwardContext == TextPointerContext.None)
break;
if (forwardContext == TextPointerContext.Text && backwardContext != TextPointerContext.Text)
break;
backwardContext = forwardContext;
_end.MoveToNextContextPosition(LogicalDirection.Forward);
}
}
// Set LogicalDirection to prevent end points from crossing a formatting
// boundary when normalized.
_start.SetLogicalDirection(LogicalDirection.Forward);
_end.SetLogicalDirection(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)
{
bool snapEndPosition = true;
if (expandStart && textView.Contains(_start))
{
TextSegment lineRange = textView.GetLineRange(_start);
if (!lineRange.IsNull)
{
// Move start position to the beginning of containing line.
if (_start.CompareTo(lineRange.Start) != 0)
{
_start = lineRange.Start.CreatePointer();
}
// If this line contains also end position, move it to the
// end of this line.
if (lineRange.Contains(_end))
{
snapEndPosition = false;
if (_end.CompareTo(lineRange.End) != 0)
{
_end = lineRange.End.CreatePointer();
}
}
}
}
if (expandEnd && snapEndPosition && textView.Contains(_end))
{
TextSegment lineRange = textView.GetLineRange(_end);
if (!lineRange.IsNull)
{
// Move end position to the end of containing line.
if (_end.CompareTo(lineRange.End) != 0)
{
_end = lineRange.End.CreatePointer();
}
}
}
}
break;
case TextUnit.Paragraph:
// Utilize TextRange logic to determine paragraph boundaries.
ITextRange textRange = new TextRange(_start, _end);
TextRangeBase.SelectParagraph(textRange, _start);
if (expandStart && _start.CompareTo(textRange.Start) != 0)
{
_start = textRange.Start.CreatePointer();
}
if (expandEnd)
{
if (!textRange.Contains(_end))
{
TextRangeBase.SelectParagraph(textRange, _end);
}
if (_end.CompareTo(textRange.End) != 0)
{
_end = textRange.End.CreatePointer();
}
}
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)
{
if (expandStart && textView.Contains(_start))
{
ITextView pageTextView = textView;
if (textView is MultiPageTextView)
{
// This is "multi page" case. Find page associated with the start position.
pageTextView = ((MultiPageTextView)textView).GetPageTextViewFromPosition(_start);
}
ReadOnlyCollection<TextSegment> textSegments = pageTextView.TextSegments;
if (textSegments != null && textSegments.Count > 0)
{
if (_start.CompareTo(textSegments[0].Start) != 0)
{
_start = textSegments[0].Start.CreatePointer();
}
}
}
if (expandEnd && textView.Contains(_end))
{
ITextView pageTextView = textView;
if (textView is MultiPageTextView)
{
// This is "multi page" case. Find page associated with the start position.
pageTextView = ((MultiPageTextView)textView).GetPageTextViewFromPosition(_end);
}
ReadOnlyCollection<TextSegment> textSegments = pageTextView.TextSegments;
if (textSegments != null && textSegments.Count > 0)
{
if (_end.CompareTo(textSegments[textSegments.Count - 1].End) != 0)
{
_end = textSegments[textSegments.Count - 1].End.CreatePointer();
}
}
}
}
break;
case TextUnit.Document:
if (expandStart && _start.CompareTo(_start.TextContainer.Start) != 0)
{
_start = _start.TextContainer.Start.CreatePointer();
}
if (expandEnd && _end.CompareTo(_start.TextContainer.End) != 0)
{
_end = _start.TextContainer.End.CreatePointer();
}
break;
default:
// Unknown unit
break;
}
}
/// <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>
/// Finds the value of a given attribute within the range.
/// </summary>
private object GetAttributeValue(TextAttributeHelper attr)
{
ITextPointer start = _start.CreatePointer();
ITextPointer end = _end.CreatePointer();
if (start.CompareTo(end) < 0)
{
// If we get plunked just outside an element we need to move ourselves inside
// before we check our start and end values.
while (IsElementBoundary(start.GetPointerContext(LogicalDirection.Forward)))
{
if (!start.MoveToNextContextPosition(LogicalDirection.Forward) || start.CompareTo(end) >= 0)
{
break;
}
}
while (IsElementBoundary(end.GetPointerContext(LogicalDirection.Backward)))
{
if (!end.MoveToNextContextPosition(LogicalDirection.Backward) || start.CompareTo(end) >= 0)
{
break;
}
}
if (start.CompareTo(end) > 0)
{
return AutomationElementIdentifiers.NotSupported;
}
}
// Get value at range end and check if it ever changes through the range.
object valueAtEndPos = attr.GetValueAt(end);
while (start.CompareTo(end) < 0 && attr.AreEqual(valueAtEndPos, attr.GetValueAt(start)))
{
if (!start.MoveToNextContextPosition(LogicalDirection.Forward) || start.CompareTo(end) > 0)
{
break;
}
}
return (start.CompareTo(end) >= 0) ? valueAtEndPos : TextPatternIdentifiers.MixedAttributeValue;
}
/// <summary>
/// Checks whether symbol type refers to element boundary.
/// </summary>
private bool IsElementBoundary(TextPointerContext symbolType)
{
return ((symbolType == TextPointerContext.ElementStart) || (symbolType == TextPointerContext.ElementEnd));
}
/// <summary>
/// Converts Brush to Color
/// </summary>
private static int ColorFromBrush(object brush)
{
SolidColorBrush solidBrush = brush as SolidColorBrush;
Color color = (solidBrush != null) ? solidBrush.Color : Colors.Black;
return (0 + (color.R << 16) + (color.G << 8) + color.B);
}
/// <summary>
/// Retrieves FontFamily name.
/// </summary>
private static string GetFontFamilyName(FontFamily fontFamily, ITextPointer context)
{
if (fontFamily != null)
{
// Typical case: return the family name/URI used to construct the FontFamily.
if (fontFamily.Source != null)
return fontFamily.Source;
// Use the target font specified by the first family map with a compatible language.
if (fontFamily.FamilyMaps != null)
{
XmlLanguage textLanguage = (context != null) ?
(XmlLanguage)context.GetValue(FrameworkElement.LanguageProperty) :
null;
foreach (FontFamilyMap familyMap in fontFamily.FamilyMaps)
{
// A language-neutral family map matches any text language.
if (familyMap.Language == null)
return familyMap.Target;
// Does the language match the text culture or a parent culture?
if (textLanguage != null && familyMap.Language.RangeIncludes(textLanguage))
return familyMap.Target;
}
}
}
// Worst case: we have to return something so just return a default family name.
return _defaultFamilyName;
}
/// <summary>
/// Retrieves TextDecoration color.
/// </summary>
private static int GetTextDecorationColor(TextDecorationCollection decorations, TextDecorationLocation location)
{
if (decorations == null)
{
return 0;
}
int color = 0;
foreach (TextDecoration decor in decorations)
{
if (decor.Location == location)
{
if (decor.Pen != null)
{
color = ColorFromBrush(decor.Pen.Brush);
// Ignore other decorations and their coloring if there're more at the same location.
break;
}
}
}
return color;
}
/// <summary>
/// Retrieves TextDecoration style.
/// </summary>
private static TextDecorationLineStyle GetTextDecorationLineStyle(TextDecorationCollection decorations, TextDecorationLocation location)
{
if (decorations == null)
{
return TextDecorationLineStyle.None;
}
TextDecorationLineStyle lineStyle = TextDecorationLineStyle.None;
foreach (TextDecoration decor in decorations)
{
if (decor.Location == location)
{
if (lineStyle == TextDecorationLineStyle.None)
{
// There's a whole bunch of all kinds of custom styles defined in TextDecorationLineStyle
// including WordsOnly, Double, Wavy, DoubleWavy, ThickWavy, which we can not determine from
// TextDecoration anyway. Hence, it seems would be too much bang for a buck if we try
// to guess out the other dozen by analyzing TextDecoration.Pen. Let's keep it simple
// and make difference only between solid and dashed lines.
if (decor.Pen != null)
{
lineStyle = (decor.Pen.DashStyle.Dashes.Count > 1) ? TextDecorationLineStyle.Dash : TextDecorationLineStyle.Single;
}
else
{
lineStyle = TextDecorationLineStyle.Single;
}
}
else
{
lineStyle = TextDecorationLineStyle.Other;
break;
}
}
}
return lineStyle;
}
/// <summary>
/// Converts Avalon units to points
/// </summary>
private static double NativeObjectLengthToPoints(double length)
{
return (double.IsNaN(length) ? 0d : (length * 72.0 / 96.0));
}
/// <summary>
/// Retrieves AutomationPeer enclosing entire range.
/// Also makes sure automation tree is properly created.
/// </summary>
/// <returns></returns>
private AutomationPeer GetEnclosingAutomationPeer(ITextPointer start, ITextPointer end)
{
// Retrieve element enclosing the entire range.
ITextPointer elementStart, elementEnd;
AutomationPeer peer = TextContainerHelper.GetEnclosingAutomationPeer(start, end, out elementStart, out elementEnd);
// If no AutomationPeer is found, assume the owner of TextPattern.
// Otherwise make sure that the AutomationPeer is properly connected
// through automation tree.
if (peer == null)
{
peer = _textPeer;
}
else
{
Invariant.Assert(elementStart != null && elementEnd != null);
AutomationPeer peerParent = GetEnclosingAutomationPeer(elementStart, elementEnd);
GetAutomationPeersFromRange(peerParent, elementStart, elementEnd);
}
return peer;
}
/// <summary>
/// Retrieves automation provider from AutomationPeer.
/// </summary>
private IRawElementProviderSimple ProviderFromPeer(AutomationPeer peer)
{
IRawElementProviderSimple provider;
if (_textPeer is TextAutomationPeer)
{
provider = ((TextAutomationPeer)_textPeer).ProviderFromPeer(peer);
}
else
{
provider = ((ContentTextAutomationPeer)_textPeer).ProviderFromPeer(peer);
}
return provider;
}
/// <summary>
/// Retrieves AutomationPeers from specified range.
/// </summary>
private List<AutomationPeer> GetAutomationPeersFromRange(AutomationPeer peer, ITextPointer start, ITextPointer end)
{
List<AutomationPeer> peers;
Invariant.Assert(peer is TextAutomationPeer || peer is ContentTextAutomationPeer);
if (peer is TextAutomationPeer)
{
peers = ((TextAutomationPeer)peer).GetAutomationPeersFromRange(start, end);
}
else
{
peers = ((ContentTextAutomationPeer)peer).GetAutomationPeersFromRange(start, end);
}
return peers;
}
/// <summary>
/// Helper function to check if given position is at word boundary. TextPointerBase.IsAtWordBoundary
/// cannot be used for not normalized positions, because it will always return TRUE, but in fact such
/// position is not in world boundary.
/// </summary>
private static bool IsAtWordBoundary(ITextPointer position)
{
if (!TextPointerBase.IsAtInsertionPosition(position))
{
return false;
}
// Note that we always use Forward direction for word orientation.
return TextPointerBase.IsAtWordBoundary(position, LogicalDirection.Forward);
}
/// <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;
}
/// <summary>
/// Ensures that the start and end points of this range are at valid insertion (caret)
/// positions.
/// </summary>
private void Normalize()
{
MoveToInsertionPosition(_start, _start.LogicalDirection);
MoveToInsertionPosition(_end, _end.LogicalDirection);
// If start passes end, move the entire range to the start position.
if (_start.CompareTo(_end) > 0)
{
_end.MoveToPosition(_start);
}
}
/// <summary>
/// Returns a normalized copy of a position -- the position at the closest legal
/// caret position in a given direction.
/// </summary>
private ITextPointer GetInsertionPosition(ITextPointer position, LogicalDirection direction)
{
position = position.CreatePointer();
MoveToInsertionPosition(position, direction);
return position;
}
#endregion Private Methods
//-------------------------------------------------------------------
//
// Private Fields
//
//-------------------------------------------------------------------
#region Private Fields
private ITextPointer _start;
private ITextPointer _end;
private TextAdaptor _textAdaptor;
private AutomationPeer _textPeer;
private static Hashtable _textPatternAttributes;
private const string _defaultFamilyName = "Global User Interface";
#endregion Private Fields
//-------------------------------------------------------------------
//
// Private Types
//
//-------------------------------------------------------------------
#region Private Types
/// <summary>
///
/// </summary>
private class TextAttributeHelper
{
internal delegate object GetValueAtDelegate(ITextPointer textPointer);
internal delegate bool AreEqualDelegate(object val1, object val2);
internal TextAttributeHelper(GetValueAtDelegate getValueDelegate, AreEqualDelegate areEqualDelegate)
{
_getValueDelegate = getValueDelegate;
_areEqualDelegate = areEqualDelegate;
}
internal GetValueAtDelegate GetValueAt { get { return _getValueDelegate; } }
internal AreEqualDelegate AreEqual { get { return _areEqualDelegate; } }
private GetValueAtDelegate _getValueDelegate;
private AreEqualDelegate _areEqualDelegate;
}
#endregion Private Types
//-------------------------------------------------------------------
//
// ITextRangeProvider
//
//-------------------------------------------------------------------
#region ITextRangeProvider
/// <summary>
/// Retrieves a new range covering an identical span of text. The new range can be manipulated independently from the original.
/// </summary>
/// <returns>The new range.</returns>
ITextRangeProvider ITextRangeProvider.Clone()
{
return new TextRangeAdaptor(_textAdaptor, _start, _end, _textPeer);
}
/// <summary>
/// Compares this range with another range.
/// </summary>
/// <param name="range">A range to compare.
/// The range must have come from the same text provider or an InvalidArgumentException will be thrown.</param>
/// <returns>true if both ranges span the same text.</returns>
bool ITextRangeProvider.Compare(ITextRangeProvider range)
{
ArgumentNullException.ThrowIfNull(range);
TextRangeAdaptor rangeAdaptor = ValidateAndThrow(range);
Normalize();
rangeAdaptor.Normalize();
return (rangeAdaptor._start.CompareTo(_start) == 0 && rangeAdaptor._end.CompareTo(_end) == 0);
}
/// <summary>
/// Compares the endpoint of this range with the endpoint of another range.
/// </summary>
/// <param name="endpoint">The endpoint of this range to compare.</param>
/// <param name="targetRange">The range with the other endpoint to compare.
/// The range must have come from the same text provider or an InvalidArgumentException will be thrown.</param>
/// <param name="targetEndpoint">The endpoint on the other range to compare.</param>
/// <returns>Returns <0 if this endpoint occurs earlier in the text than the target endpoint.
/// Returns 0 if this endpoint is at the same location as the target endpoint.
/// Returns >0 if this endpoint occurs later in the text than the target endpoint.</returns>
int ITextRangeProvider.CompareEndpoints(TextPatternRangeEndpoint endpoint, ITextRangeProvider targetRange, TextPatternRangeEndpoint targetEndpoint)
{
ArgumentNullException.ThrowIfNull(targetRange);
TextRangeAdaptor rangeAdaptor = ValidateAndThrow(targetRange);
Normalize();
rangeAdaptor.Normalize();
ITextPointer position = (endpoint == TextPatternRangeEndpoint.Start) ? _start : _end;
ITextPointer targetPosition = (targetEndpoint == TextPatternRangeEndpoint.Start) ? rangeAdaptor._start : rangeAdaptor._end;
return position.CompareTo(targetPosition);
}
/// <summary>
/// Expands the range to an integral number of enclosing units. this could be used, for example,
/// to guarantee that a range endpoint is not in the middle of a word. If the range is already an
/// integral number of the specified units then it remains unchanged.
/// </summary>
/// <param name="unit">The textual unit.</param>
void ITextRangeProvider.ExpandToEnclosingUnit(TextUnit unit)
{
Normalize();
// TextPattern spec update: End EndPoint moves to the end of the same TextUnit that
// Start EndPoint is within (whether it was within that unit originally or not).
// To support this scenario always move _end to the next position after _start.
_end.MoveToPosition(_start);
_end.MoveToNextInsertionPosition(LogicalDirection.Forward);
ExpandToEnclosingUnit(unit, true, true);
}
/// <summary>
/// Searches for a subrange of text that has the specified attribute. To search the entire document use the text provider's
/// document range.
/// </summary>
/// <param name="attributeId">The attribute to search for.</param>
/// <param name="value">The value of the specified attribute to search for.</param>
/// <param name="backward">true if the last occurring range should be returned instead of the first.</param>
/// <returns>A subrange with the specified attribute, or null if no such subrange exists.</returns>
ITextRangeProvider ITextRangeProvider.FindAttribute(int attributeId, object value, bool backward)
{
AutomationTextAttribute attribute = AutomationTextAttribute.LookupById(attributeId);
if (attribute == null)
{
throw new ArgumentNullException("attributeId");
}
ArgumentNullException.ThrowIfNull(value);
if (!_textPatternAttributes.ContainsKey(attribute))
{
return null;
}
Normalize();
ITextRangeProvider resultRange = null;
ITextPointer attrStart = null;
ITextPointer attrEnd = null;
TextAttributeHelper attr = (TextAttributeHelper)_textPatternAttributes[attribute];
if (backward)
{
ITextPointer stop = _start;
ITextPointer position = _end.CreatePointer(LogicalDirection.Backward);
// Go backward from the range end position until we find a position that
// has our attribute or we hit the start position of the search range.
attrStart = stop;
while (position.CompareTo(stop) > 0)
{
if (attr.AreEqual(value, attr.GetValueAt(position)))
{
if (attrEnd == null)
{
attrEnd = position.CreatePointer(LogicalDirection.Backward);
}
}
else
{
if (attrEnd != null)
{
attrStart = position.CreatePointer(LogicalDirection.Forward);
break;
}
}
if (!position.MoveToNextContextPosition(LogicalDirection.Backward))
{
break;
}
}
}
else
{
ITextPointer stop = _end;
ITextPointer position = _start.CreatePointer(LogicalDirection.Forward);
// Go backward from the range end position until we find a position that
// has our attribute or we hit the start position of the search range.
attrEnd = stop;
while (position.CompareTo(stop) < 0)
{
if (attr.AreEqual(value, attr.GetValueAt(position)))
{
if (attrStart == null)
{
attrStart = position.CreatePointer(LogicalDirection.Forward);
}
}
else
{
if (attrStart != null)
{
attrEnd = position.CreatePointer(LogicalDirection.Backward);
break;
}
}
if (!position.MoveToNextContextPosition(LogicalDirection.Forward))
{
break;
}
}
}
if (attrStart != null && attrEnd != null)
{
resultRange = new TextRangeAdaptor(_textAdaptor, attrStart, attrEnd, _textPeer);
}
return resultRange;
}
/// <summary>
/// Searches for an occurrence of text within the range.
/// </summary>
/// <param name="text">The text to search for.</param>
/// <param name="backward">true if the last occurring range should be returned instead of the first.</param>
/// <param name="ignoreCase">true if case should be ignored for the purposes of comparison.</param>
/// <returns>A subrange with the specified text, or null if no such subrange exists.</returns>
ITextRangeProvider ITextRangeProvider.FindText(string text, bool backward, bool ignoreCase)
{
ArgumentNullException.ThrowIfNull(text);
if (text.Length == 0)
{
throw new ArgumentException(SR.Format(SR.TextRangeProvider_EmptyStringParameter, "text"));
}
Normalize();
if (_start.CompareTo(_end) == 0)
{
return null;
}
TextRangeAdaptor range = null;
FindFlags findFlags = FindFlags.None;
if (!ignoreCase)
{
findFlags |= FindFlags.MatchCase;
}
if (backward)
{
findFlags |= FindFlags.FindInReverse;
}
ITextRange findResult = TextFindEngine.Find(_start, _end, text, findFlags, CultureInfo.CurrentCulture);
if (findResult != null && !findResult.IsEmpty)
{
range = new TextRangeAdaptor(_textAdaptor, findResult.Start, findResult.End, _textPeer);
}
return range;
}
/// <summary>
/// Retrieves the value of a text attribute over the entire range.
/// </summary>
/// <param name="attributeId">The text attribute.</param>
/// <returns>The value of the attribute across the range.
/// If the attribute's value varies over the range then the value is TextPattern.MixedAttributeValue</returns>
object ITextRangeProvider.GetAttributeValue(int attributeId)
{
AutomationTextAttribute attribute = AutomationTextAttribute.LookupById(attributeId);
// In Windows 8, a new text attribute was introduced that WPF does not have any reference for
// this caused WPF to throw an ArgumentException. This code path was strange as we already can
// return NotSupported, which is a valid response. So change this to no longer throw.
if (attribute == null || !_textPatternAttributes.ContainsKey(attribute))
{
return AutomationElementIdentifiers.NotSupported;
}
Normalize();
return GetAttributeValue((TextAttributeHelper)_textPatternAttributes[attribute]);
}
/// <summary>
/// Retrieves the bounding rectangles for viewable lines of the range.
/// </summary>
/// <returns>An array of bounding rectangles for each line or portion of a line within the client area of the text provider.
/// No bounding rectangles will be returned for lines that are empty or scrolled out of view. Note that even though a
/// bounding rectangle is returned the corresponding text may not be visible due to overlapping windows.
/// This will not return null, but may return an empty array.</returns>
double[] ITextRangeProvider.GetBoundingRectangles()
{
Normalize();
Rect[] rects = _textAdaptor.GetBoundingRectangles(_start, _end, true, true);
double[] asDoubles = new double[rects.Length * 4];
for (int i = 0; i < rects.Length; i++)
{
asDoubles[4 * i] = rects[i].X;
asDoubles[4 * i + 1] = rects[i].Y;
asDoubles[4 * i + 2] = rects[i].Width;
asDoubles[4 * i + 3] = rects[i].Height;
}
return asDoubles;
}
/// <summary>
/// Retrieves the innermost element that encloses this range.
/// </summary>
/// <returns>An element. Usually this element will be the one that supplied this range.
/// However, if the text provider supports child elements such as tables or hyperlinks, then the
/// enclosing element could be a descendant element of the text provider.
/// </returns>
IRawElementProviderSimple ITextRangeProvider.GetEnclosingElement()
{
Normalize();
AutomationPeer peer = GetEnclosingAutomationPeer(_start, _end);
Invariant.Assert(peer != null);
IRawElementProviderSimple provider = ProviderFromPeer(peer);
Invariant.Assert(provider != null);
return provider;
}
/// <summary>
/// Retrieves the text of the range.
/// </summary>
/// <param name="maxLength">Specifies the maximum length of the string to return or -1 if no limit is requested.</param>
/// <returns>The text of the range possibly truncated to the specified limit.</returns>
string ITextRangeProvider.GetText(int maxLength)
{
if (maxLength < 0 && maxLength != -1)
{
throw new ArgumentException(SR.Format(SR.TextRangeProvider_InvalidParameterValue, maxLength, "maxLength"));
}
Normalize();
string text = TextRangeBase.GetTextInternal(_start, _end);
return (text.Length <= maxLength || maxLength == -1) ? text : text.Substring(0, maxLength);
}
/// <summary>
/// Moves the range the specified number of units in the text. Note that the text is not altered. Instead the
/// range spans a different part of the text.
/// If the range is degenerate, this method tries to move the insertion point count units. If the range is nondegenerate
/// and count is greater than zero, this method collapses the range at its end point, moves the resulting range forward
/// to a unit boundary (if it is not already at one), and then tries to move count - 1 units forward. If the range is
/// nondegenerate and count is less than zero, this method collapses the range at the starting point, moves the resulting
/// range backward to a unit boundary (if it isn't already at one), and then tries to move |count| - 1 units backward.
/// Thus, in both cases, collapsing a nondegenerate range, whether or not moving to the start or end of the unit following
/// the collapse, counts as a unit.
/// </summary>
/// <param name="unit">The textual unit for moving.</param>
/// <param name="count">The number of units to move. A positive count moves the range forward.
/// A negative count moves backward. A count of 0 has no effect.</param>
/// <returns>The number of units actually moved, which can be less than the number requested if
/// moving the range runs into the beginning or end of the document.</returns>
int ITextRangeProvider.Move(TextUnit unit, int count)
{
Normalize();
int movedCount = 0;
// Do not expand range for Paragraphs, because TextRange.SelectParagraph will take care of it.
if (unit != TextUnit.Paragraph)
{
ExpandToEnclosingUnit(unit, true, true);
}
if (count != 0)
{
// Move start position by number of units.
ITextPointer position = _start.CreatePointer();
movedCount = MovePositionByUnits(position, unit, count);
// If endpoint has been moved at least by one unit or its direction has changed, snap it to TextUnit boundary,
// because movement done by MovePositionByUnits does not guarantee position snapping.
if ((position.CompareTo(_start)==0 && position.LogicalDirection != _start.LogicalDirection) ||
(count > 0 && position.CompareTo(_start) > 0) ||
(count < 0 && position.CompareTo(_start) < 0))
{
_start = position;
// Move end position by 1 offset forward, so it does not point to _start.
// Later ExpandToEnclosingUnit will position it at appropriate unit boundary.
_end = position.CreatePointer();
if (unit != TextUnit.Page)
{
_end.MoveToNextInsertionPosition(LogicalDirection.Forward);
}
ExpandToEnclosingUnit(unit, true, true);
// If endpoint has been moved, but 'movedCount' is 0, it means that we snapped to neariest
// unit boundary. Treat this situation as actual move.
if (movedCount == 0)
{
movedCount = (count > 0) ? 1 : -1;
}
}
}
return movedCount;
}
/// <summary>
/// Moves one endpoint of the range the specified number of units in the text.
/// If the endpoint being moved crosses the other endpoint then the other endpoint
/// is moved along too resulting in a degenerate range and ensuring the correct ordering
/// of the endpoints. (i.e. always Start<=End)
/// </summary>
/// <param name="endpoint">The endpoint to move.</param>
/// <param name="unit">The textual unit for moving.</param>
/// <param name="count">The number of units to move. A positive count moves the endpoint forward.
/// A negative count moves backward. A count of 0 has no effect.</param>
/// <returns>The number of units actually moved, which can be less than the number requested if
/// moving the endpoint runs into the beginning or end of the document.</returns>
int ITextRangeProvider.MoveEndpointByUnit(TextPatternRangeEndpoint endpoint, TextUnit unit, int count)
{
Normalize();
int movedCount = 0;
if (count != 0)
{
// Move endpoint by number of units.
bool start = (endpoint == TextPatternRangeEndpoint.Start);
ITextPointer positionRef = start ? _start : _end;
ITextPointer position = positionRef.CreatePointer();
if (MoveToUnitBoundary(position, start, count < 0 ? LogicalDirection.Backward : LogicalDirection.Forward, unit))
{
movedCount = (count > 0) ? 1 : -1;
}
if (count != movedCount)
{
movedCount += MovePositionByUnits(position, unit, count - movedCount);
}
// If endpoint has been moved at least by one unit, snap it to TextUnit boundary,
// because movement done by MovePositionByUnits does not guarantee position snapping.
if ((count > 0 && position.CompareTo(positionRef) > 0) ||
(count < 0 && position.CompareTo(positionRef) < 0) ||
(position.CompareTo(positionRef) == 0 && position.LogicalDirection != positionRef.LogicalDirection))
{
if (start)
{
_start = position;
}
else
{
_end = position;
}
if (unit != TextUnit.Page)
{
ExpandToEnclosingUnit(unit, start, !start);
}
// If endpoint has been moved, but 'movedCount' is 0, it means that we snapped to neariest
// unit boundary. Treat this situation as actual move.
if (movedCount == 0)
{
movedCount = (count > 0) ? 1 : -1;
}
}
// Ensure the correct ordering of the endpoint.
if (_start.CompareTo(_end) > 0)
{
if (start)
{
_end = _start.CreatePointer();
}
else
{
_start = _end.CreatePointer();
}
}
}
return movedCount;
}
/// <summary>
/// Moves an endpoint of this range to coincide with the endpoint of another range.
/// </summary>
/// <param name="endpoint">The endpoint to move.</param>
/// <param name="targetRange">Another range from the same text provider.</param>
/// <param name="targetEndpoint">An endpoint on the other range.</param>
void ITextRangeProvider.MoveEndpointByRange(TextPatternRangeEndpoint endpoint, ITextRangeProvider targetRange, TextPatternRangeEndpoint targetEndpoint)
{
ArgumentNullException.ThrowIfNull(targetRange);
TextRangeAdaptor rangeAdaptor = ValidateAndThrow(targetRange);
ITextPointer targetPointer = (targetEndpoint == TextPatternRangeEndpoint.Start) ? rangeAdaptor._start : rangeAdaptor._end;
if (endpoint == TextPatternRangeEndpoint.Start)
{
_start = targetPointer.CreatePointer();
// Ensure the correct ordering of the endpoint.
if (_start.CompareTo(_end) > 0)
{
_end = _start.CreatePointer();
}
}
else
{
_end = targetPointer.CreatePointer();
// Ensure the correct ordering of the endpoint.
if (_start.CompareTo(_end) > 0)
{
_start = _end.CreatePointer();
}
}
}
/// <summary>
/// Selects the text of the range within the provider. If the provider does not have a concept of selection then
/// it should return false for ITextProvider.SupportsTextSelection property and throw an InvalidOperation
/// exception for this method.
/// </summary>
void ITextRangeProvider.Select()
{
if (((ITextProvider)_textAdaptor).SupportedTextSelection == SupportedTextSelection.None)
{
throw new InvalidOperationException(SR.TextProvider_TextSelectionNotSupported);
}
Normalize();
_textAdaptor.Select(_start, _end);
}
/// <summary>
/// Adds the text of the range to the current selection. If the provider does not have a concept of selection
/// or does not support multiple disjoint selection then it throw an InvalidOperation
/// exception for this method.
/// </summary>
void ITextRangeProvider.AddToSelection()
{
throw new InvalidOperationException();
}
/// <summary>
/// Removes the text of the range from the current selection. If the provider does not have a concept of selection
/// or does not support multiple disjoint selection then it throw an InvalidOperation
/// exception for this method.
/// </summary>
void ITextRangeProvider.RemoveFromSelection()
{
throw new InvalidOperationException();
}
/// <summary>
/// Scrolls the text in the provider so the range is within the viewport.
/// </summary>
/// <param name="alignToTop">true if the provider should be scrolled so the range is flush with the top of the viewport.
/// false if the provider should be scrolled so the range is flush with the bottom.</param>
void ITextRangeProvider.ScrollIntoView(bool alignToTop)
{
Normalize();
_textAdaptor.ScrollIntoView(_start, _end, alignToTop);
}
/// <summary>
/// Retrieves a collection of all of the children that fall within the range.
/// </summary>
/// <returns>An enumeration of all children that fall within the range. Children
/// that overlap with the range but are not entirely enclosed by it will
/// also be included in the collection. If there are no children then
/// this can return either null or an empty enumeration.</returns>
IRawElementProviderSimple[] ITextRangeProvider.GetChildren()
{
Normalize();
IRawElementProviderSimple[] elements = null;
AutomationPeer peer = GetEnclosingAutomationPeer(_start, _end);
Invariant.Assert(peer != null);
List<AutomationPeer> peers = GetAutomationPeersFromRange(peer, _start, _end);
if (peers.Count > 0)
{
elements = new IRawElementProviderSimple[peers.Count];
for (int i = 0; i < peers.Count; i++)
{
elements[i] = ProviderFromPeer(peers[i]);
}
}
return elements;
}
#endregion ITextRangeProvider
}
}
|