|
// 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:
// ITextRangeProvider interface for WindowsEditBox
// PRESHARP: In order to avoid generating warnings about unkown message numbers and unknown pragmas.
#pragma warning disable 1634, 1691
using System;
using System.Collections;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Input;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Automation.Provider;
using System.Windows.Automation.Text;
using System.ComponentModel;
using MS.Win32;
namespace MS.Internal.AutomationProxies
{
// TERMINOLOGY: Win32 Edit controls use the term "index" to mean a character position.
// For example the EM_LINEINDEX message converts a line number to it's starting character position.
// Perhaps not the best choice but we use it to be consistent.
internal class WindowsEditBoxRange : ITextRangeProvider
{
//------------------------------------------------------
//
// Constructor
//
//------------------------------------------------------
internal WindowsEditBoxRange(WindowsEditBox provider, int start, int end)
{
if (start < 0 || end < start)
{
// i'm throwing an invalid operation exception rather than an argument exception because
// clients never call this constructor directly. it always happens as a result of some
// other operation, e.g. cloning an existing TextPatternRange.
throw new InvalidOperationException(SR.Format(SR.InvalidTextRangeOffset, GetType().FullName));
}
Debug.Assert(provider != null);
_provider = provider;
_start = start;
_end = end;
}
//------------------------------------------------------
//
// Public Methods
//
//------------------------------------------------------
#region Public Methods
ITextRangeProvider ITextRangeProvider.Clone()
{
return new WindowsEditBoxRange(_provider, Start, End);
}
bool ITextRangeProvider.Compare(ITextRangeProvider range)
{
// TextPatternRange already verifies the other range comes from the same element before forwarding so we only need to worry about
// whether the endpoints are identical.
WindowsEditBoxRange editRange = (WindowsEditBoxRange)range;
return editRange.Start == Start && editRange.End == End;
}
int ITextRangeProvider.CompareEndpoints(TextPatternRangeEndpoint endpoint, ITextRangeProvider targetRange, TextPatternRangeEndpoint targetEndpoint)
{
// TextPatternRange already verifies the other range comes from the same element before forwarding so we only need to worry about
// comparing the endpoints.
WindowsEditBoxRange editRange = (WindowsEditBoxRange)targetRange;
int e1 = (endpoint == TextPatternRangeEndpoint.Start) ? Start : End;
int e2 = (targetEndpoint == TextPatternRangeEndpoint.Start) ? editRange.Start : editRange.End;
return e1 - e2;
}
void ITextRangeProvider.ExpandToEnclosingUnit(TextUnit unit)
{
Misc.SetFocus(_provider._hwnd);
switch (unit)
{
case TextUnit.Character:
// if it is a degenerate range then expand it to be one character.
// otherwise, leave it as it is.
if (Start == End)
{
int moved;
End = MoveEndpointForward(End, TextUnit.Character, 1, out moved);
}
break;
case TextUnit.Word:
{
// this works same as paragraph except we look for word boundaries instead of paragraph boundaries.
// get the text so we can figure out where the boundaries are
string text = _provider.GetText();
ValidateEndpoints();
#if WCP_NLS_ENABLED
// use the same word breaker that Avalon Text uses.
WordBreaker breaker = new WordBreaker();
TextContainer container = new TextContainer(text);
// if the starting point of the range is not already at a word break
// then move it backwards to the nearest word break.
TextNavigator startNavigator = new TextNavigator(Start, container);
if (!breaker.IsAtWordBreak(startNavigator))
{
breaker.MoveToPreviousWordBreak(startNavigator);
Start = startNavigator.Position;
}
// if the range is degenerate or the ending point of the range is not already at a word break
// then move it forwards to the nearest word break.
TextNavigator endNavigator = new TextNavigator(End, container);
if (Start==End || !breaker.IsAtWordBreak(endNavigator))
{
breaker.MoveToNextWordBreak(endNavigator);
End = endNavigator.Position;
}
#else
// move start left until we reach a word boundary.
for (; !AtWordBoundary(text, Start); Start--) ;
// move end right until we reach word boundary (different from Start).
End = Math.Min(Math.Max(End, Start + 1), text.Length);
for (; !AtWordBoundary(text, End); End++) ;
#endif
}
break;
case TextUnit.Line:
{
if (_provider.GetLineCount() != 1)
{
int startLine = _provider.LineFromChar(Start);
int endLine = _provider.LineFromChar(End);
MoveTo(_provider.LineIndex(startLine), _provider.LineIndex(endLine + 1));
}
else
{
MoveTo(0, _provider.GetTextLength());
}
}
break;
case TextUnit.Paragraph:
{
// this works same as paragraph except we look for word boundaries instead of paragraph boundaries.
// get the text so we can figure out where the boundaries are
string text = _provider.GetText();
ValidateEndpoints();
// move start left until we reach a paragraph boundary.
for (; !AtParagraphBoundary(text, Start); Start--);
// move end right until we reach a paragraph boundary (different from Start).
End = Math.Min(Math.Max(End, Start + 1), text.Length);
for (; !AtParagraphBoundary(text, End); End++);
}
break;
case TextUnit.Format:
case TextUnit.Page:
case TextUnit.Document:
MoveTo(0, _provider.GetTextLength());
break;
//break;
default:
throw new System.ComponentModel.InvalidEnumArgumentException("unit", (int)unit, typeof(TextUnit));
}
}
ITextRangeProvider ITextRangeProvider.FindAttribute(int attributeId, object val, bool backwards)
{
AutomationTextAttribute attribute = AutomationTextAttribute.LookupById(attributeId);
// generic controls are plain text so if the attribute matches then it matches over the whole range.
// To workaround the conversion that Marshaling of COM-interop did.
object targetAttribute = GetAttributeValue(attribute);
if (targetAttribute is Enum)
{
targetAttribute = (int)targetAttribute;
}
return val.Equals(targetAttribute) ? new WindowsEditBoxRange(_provider, Start, End) : null;
}
ITextRangeProvider ITextRangeProvider.FindText(string text, bool backwards, bool ignoreCase)
{
ArgumentException.ThrowIfNullOrEmpty(text);
// get the text of the range
string rangeText = _provider.GetText();
ValidateEndpoints();
rangeText = rangeText.Substring(Start, Length);
// if we are ignoring case then convert everything to lowercase
if (ignoreCase)
{
rangeText = rangeText.ToLower(System.Globalization.CultureInfo.InvariantCulture);
text = text.ToLower(System.Globalization.CultureInfo.InvariantCulture);
}
// do a case-sensitive search for the text inside the range.
int i = backwards ? rangeText.LastIndexOf(text, StringComparison.Ordinal) : rangeText.IndexOf(text, StringComparison.Ordinal);
// if the text was found then create a new range covering the found text.
return i >= 0 ? new WindowsEditBoxRange(_provider, Start + i, Start + i + text.Length) : null;
}
object ITextRangeProvider.GetAttributeValue(int attributeId)
{
AutomationTextAttribute attribute = AutomationTextAttribute.LookupById(attributeId);
return GetAttributeValue(attribute);
}
double[] ITextRangeProvider.GetBoundingRectangles()
{
// Return zero rectangles for a degenerate range. We don't return an empty,
// but properly positioned, rectangle for degenerate ranges because
// there is ambiguity at line breaks and some international scenarios.
if (IsDegenerate)
{
return Array.Empty<double>();
}
// we'll need to have the text eventually (so we can measure characters) so get it up
// front so we can check the endpoints before proceeding.
string text = _provider.GetText();
ValidateEndpoints();
// get the mapping from client coordinates to screen coordinates
NativeMethods.Win32Point w32point;
w32point.x = 0;
w32point.y = 0;
if (!Misc.MapWindowPoints(_provider.WindowHandle, IntPtr.Zero, ref w32point, 1))
{
return Array.Empty<double>();
}
Point mapClientToScreen = new Point(w32point.x, w32point.y);
// clip the rectangles to the edit control's formatting rectangle
Rect clippingRectangle = _provider.GetRect();
// we accumulate rectangles onto a list
ArrayList rects;
if (_provider.IsMultiline)
{
rects = GetMultilineBoundingRectangles(text, mapClientToScreen, clippingRectangle);
}
else
{
// single line edit control
rects = new ArrayList(1);
// figure out the rectangle for this one line
Point startPoint = _provider.PosFromChar(Start);
Point endPoint = _provider.PosFromCharUR(End - 1, text);
Rect rect = new Rect(startPoint.X, startPoint.Y, endPoint.X - startPoint.X, clippingRectangle.Height);
rect.Intersect(clippingRectangle);
// use the rectangle if it is non-empty.
if (rect.Width > 0 && rect.Height > 0) // r.Empty is true only if both width & height are zero. Duh!
{
rect.Offset(mapClientToScreen.X, mapClientToScreen.Y);
rects.Add(rect);
}
}
// convert the list of rectangles into an array for returning
Rect[] rectArray = new Rect[rects.Count];
rects.CopyTo(rectArray);
return Misc.RectArrayToDoubleArray(rectArray);
}
IRawElementProviderSimple ITextRangeProvider.GetEnclosingElement()
{
return _provider;
}
string ITextRangeProvider.GetText(int maxLength)
{
if (maxLength < 0)
maxLength = End;
string text = _provider.GetText();
ValidateEndpoints();
return text.Substring(Start, maxLength >= 0 ? Math.Min(Length, maxLength) : Length);
}
int ITextRangeProvider.Move(TextUnit unit, int count)
{
Misc.SetFocus(_provider._hwnd);
// positive count means move forward. negative count means move backwards.
int moved = 0;
if (count > 0)
{
// If the range is non-degenerate then we need to collapse the range.
// (See the discussion of Count for ITextRange::Move)
if (!IsDegenerate)
{
// If the count is greater than zero, collapse the range at its end point
Start = End;
}
// move the degenerate range forward by the number of units
int m;
int start = Start;
Start = MoveEndpointForward(Start, unit, count, out m);
// if the start did not change then no move was done.
if (start != Start)
{
moved = m;
}
}
else if (count < 0)
{
// If the range is non-degenerate then we need to collapse the range.
if (!IsDegenerate)
{
// If the count is less than zero, collapse the range at the starting point
End = Start;
}
// move the degenerate range backward by the number of units
int m;
int end = End;
End = MoveEndpointBackward(End, unit, count, out m);
// if the end did not change then no move was done.
if (end != End)
{
moved = m;
}
}
else
{
// moving zero of any unit has no effect.
moved = 0;
}
return moved;
}
int ITextRangeProvider.MoveEndpointByUnit(TextPatternRangeEndpoint endpoint, TextUnit unit, int count)
{
Misc.SetFocus(_provider._hwnd);
// positive count means move forward. negative count means move backwards.
int moved = 0;
bool moveStart = endpoint == TextPatternRangeEndpoint.Start;
int start = Start;
int end = End;
if (count > 0)
{
if (moveStart)
{
Start = MoveEndpointForward(Start, unit, count, out moved);
// if the start did not change then no move was done.
if (start == Start)
{
moved = 0;
}
}
else
{
End = MoveEndpointForward(End, unit, count, out moved);
// if the end did not change then no move was done.
if (end == End)
{
moved = 0;
}
}
}
else if (count < 0)
{
if (moveStart)
{
Start = MoveEndpointBackward(Start, unit, count, out moved);
// if the start did not change then no move was done.
if (start == Start)
{
moved = 0;
}
}
else
{
End = MoveEndpointBackward(End, unit, count, out moved);
// if the end did not change then no move was done.
if (end == End)
{
moved = 0;
}
}
}
else
{
// moving zero of any unit has no effect.
moved = 0;
}
return moved;
}
void ITextRangeProvider.MoveEndpointByRange(TextPatternRangeEndpoint endpoint, ITextRangeProvider targetRange, TextPatternRangeEndpoint targetEndpoint)
{
Misc.SetFocus(_provider._hwnd);
// TextPatternRange already verifies the other range comes from the same element before forwarding so we only need to worry
// about the endpoints.
WindowsEditBoxRange editRange = (WindowsEditBoxRange)targetRange;
int e = (targetEndpoint == TextPatternRangeEndpoint.Start) ? editRange.Start : editRange.End;
if (endpoint == TextPatternRangeEndpoint.Start)
{
Start = e;
}
else
{
End = e;
}
}
void ITextRangeProvider.Select()
{
Misc.SetFocus(_provider._hwnd);
_provider.SetSel(Start, End);
}
void ITextRangeProvider.AddToSelection()
{
throw new InvalidOperationException();
}
void ITextRangeProvider.RemoveFromSelection()
{
throw new InvalidOperationException();
}
void ITextRangeProvider.ScrollIntoView(bool alignToTop)
{
Misc.SetFocus(_provider._hwnd);
// Scroll into view is handled differently depending on whether
// it is a multi-line control or not.
if (_provider.IsMultiline)
{
int newFirstLine;
if (alignToTop)
{
newFirstLine = _provider.LineFromChar(Start);
}
else
{
newFirstLine =
Math.Max(0, _provider.LineFromChar(End) - _provider.LinesPerPage() + 1);
}
_provider.LineScroll(Start, newFirstLine - _provider.GetFirstVisibleLine());
}
else if (_provider.IsScrollable)
{
Misc.SetFocus(_provider._hwnd);
int visibleStart;
int visibleEnd;
_provider.GetVisibleRangePoints(out visibleStart, out visibleEnd);
if (Misc.IsReadingRTL(_provider._hwnd))
{
short key = UnsafeNativeMethods.VK_LEFT;
if (Start > visibleStart)
{
key = UnsafeNativeMethods.VK_RIGHT;
}
while (Start > visibleStart || Start < visibleEnd)
{
Input.SendKeyboardInputVK(key, true);
_provider.GetVisibleRangePoints(out visibleStart, out visibleEnd);
}
}
else
{
short key = UnsafeNativeMethods.VK_RIGHT;
if (Start < visibleStart)
{
key = UnsafeNativeMethods.VK_LEFT;
}
while (Start < visibleStart || Start > visibleEnd)
{
Input.SendKeyboardInputVK(key, true);
_provider.GetVisibleRangePoints(out visibleStart, out visibleEnd);
}
}
}
}
#endregion Public Methods
//------------------------------------------------------
//
// Public Properties
//
//------------------------------------------------------
#region Public Properties
IRawElementProviderSimple[] ITextRangeProvider.GetChildren()
{
// we don't have any children so return an empty array
return Array.Empty<IRawElementProviderSimple>();
}
#endregion Public Properties
//------------------------------------------------------
//
// Private Methods
//
//------------------------------------------------------
#region Private Methods
// returns true iff index identifies a paragraph boundary within text.
private static bool AtParagraphBoundary(string text, int index)
{
return index <= 0 || index >= text.Length || (text[index - 1]=='\n') && (text[index]!='\n');
}
#if !WCP_NLS_ENABLED
// returns true iff index identifies a word boundary within text.
// following richedit & word precedent the boundaries are at the leading edge of the word
// so the span of a word includes trailing whitespace.
private static bool AtWordBoundary(string text, int index)
{
// NOTE: this is a heuristic word break detector that matches RichEdit behavior pretty well for
// English prose. It is a placeholder until we put in something with real wordbreaking
// intelligence based on the System.NaturalLanguage DLL.
// we are at a word boundary if we are at the beginning or end of the text
if (index <= 0 || index >= text.Length)
{
return true;
}
if( AtParagraphBoundary(text, index))
{
return true;
}
char ch1 = text[index - 1];
char ch2 = text[index];
// an apostrophe does *not* break a word if it follows or precedes characters
if ((char.IsLetterOrDigit(ch1) && IsApostrophe(ch2))
|| (IsApostrophe(ch1) && char.IsLetterOrDigit(ch2) && index >= 2 && char.IsLetterOrDigit(text[index - 2])))
{
return false;
}
// the following transitions mark boundaries.
// note: these are constructed to include trailing whitespace.
return (char.IsWhiteSpace(ch1) && !char.IsWhiteSpace(ch2))
|| (char.IsLetterOrDigit(ch1) && !char.IsLetterOrDigit(ch2))
|| (!char.IsLetterOrDigit(ch1) && char.IsLetterOrDigit(ch2))
|| (char.IsPunctuation(ch1) && char.IsWhiteSpace(ch2));
}
private static bool IsApostrophe(char ch)
{
return ch == '\'' ||
ch == (char)0x2019; // Unicode Right Single Quote Mark
}
#endif
// a big pseudo-switch statement based on the attribute
private object GetAttributeValue(AutomationTextAttribute attribute)
{
object rval;
if (attribute == TextPattern.BackgroundColorAttribute)
{
rval = GetBackgroundColor();
}
else if (attribute == TextPattern.CapStyleAttribute)
{
rval = GetCapStyle(_provider.WindowStyle);
}
else if (attribute == TextPattern.FontNameAttribute)
{
rval = GetFontName(_provider.GetLogfont());
}
else if (attribute == TextPattern.FontSizeAttribute)
{
rval = GetFontSize(_provider.GetLogfont());
}
else if (attribute == TextPattern.FontWeightAttribute)
{
rval = GetFontWeight(_provider.GetLogfont());
}
else if (attribute == TextPattern.ForegroundColorAttribute)
{
rval = GetForegroundColor();
}
else if (attribute == TextPattern.HorizontalTextAlignmentAttribute)
{
rval = GetHorizontalTextAlignment(_provider.WindowStyle);
}
else if (attribute == TextPattern.IsItalicAttribute)
{
rval = GetItalic(_provider.GetLogfont());
}
else if (attribute == TextPattern.IsReadOnlyAttribute)
{
rval = GetReadOnly();
}
else if (attribute == TextPattern.StrikethroughStyleAttribute)
{
rval = GetStrikethroughStyle(_provider.GetLogfont());
}
else if (attribute == TextPattern.UnderlineStyleAttribute)
{
rval = GetUnderlineStyle(_provider.GetLogfont());
}
else
{
rval = AutomationElement.NotSupported;
}
return rval;
}
// helper function to accumulate a list of bounding rectangles for a potentially mult-line range
private ArrayList GetMultilineBoundingRectangles(string text, Point mapClientToScreen, Rect clippingRectangle)
{
// remember the line height
int height = Math.Abs(_provider.GetLogfont().lfHeight);;
// get the starting and ending lines for the range.
int start = Start;
int end = End;
int startLine = _provider.LineFromChar(start);
int endLine = _provider.LineFromChar(end - 1);
// adjust the start based on the first visible line
int firstVisibleLine = _provider.GetFirstVisibleLine();
if (firstVisibleLine > startLine)
{
startLine = firstVisibleLine;
start = _provider.LineIndex(startLine);
}
// adjust the end based on the last visible line
int lastVisibleLine = firstVisibleLine + _provider.LinesPerPage() - 1;
if (lastVisibleLine < endLine)
{
endLine = lastVisibleLine;
end = _provider.LineIndex(endLine) - 1;
}
// adding a rectangle for each line
ArrayList rects = new ArrayList(Math.Max(endLine - startLine + 1, 0));
int nextLineIndex = _provider.LineIndex(startLine);
for (int i = startLine; i <= endLine; i++)
{
// determine the starting coordinate on this line
Point startPoint;
if (i == startLine)
{
startPoint = _provider.PosFromChar(start);
}
else
{
startPoint = _provider.PosFromChar(nextLineIndex);
}
// determine the ending coordinate on this line
Point endPoint;
if (i == endLine)
{
endPoint = _provider.PosFromCharUR(end-1, text);
}
else
{
nextLineIndex = _provider.LineIndex(i + 1);
endPoint = _provider.PosFromChar(nextLineIndex - 1);
}
// add a bounding rectangle for this line if it is nonempty
Rect rect = new Rect(startPoint.X, startPoint.Y, endPoint.X - startPoint.X, height);
rect.Intersect(clippingRectangle);
if (rect.Width > 0 && rect.Height > 0) // r.Empty is true only if both width & height are zero. Duh!
{
rect.Offset(mapClientToScreen.X, mapClientToScreen.Y);
rects.Add(rect);
}
}
return rects;
}
// returns the value of the corresponding text attribute
private static object GetHorizontalTextAlignment(int style)
{
if (Misc.IsBitSet(style, NativeMethods.ES_CENTER))
{
return HorizontalTextAlignment.Centered;
}
else if (Misc.IsBitSet(style, NativeMethods.ES_RIGHT))
{
return HorizontalTextAlignment.Right;
}
else
{
return HorizontalTextAlignment.Left;
}
}
// returns the value of the corresponding text attribute
private static object GetCapStyle(int style)
{
return Misc.IsBitSet(style, NativeMethods.ES_UPPERCASE) ? CapStyle.AllCap : CapStyle.None;
}
// returns the value of the corresponding text attribute
private object GetReadOnly()
{
return _provider.IsReadOnly();
}
// returns the value of the corresponding text attribute
private static object GetBackgroundColor()
{
// NOTE! it is possible for parents of edit controls to change the background color by responding
// to WM_CTLCOLOREDIT however we have decided not to handle that case.
return SafeNativeMethods.GetSysColor(NativeMethods.COLOR_WINDOW);
}
// returns the value of the corresponding text attribute
private static object GetFontName(NativeMethods.LOGFONT logfont)
{
return logfont.lfFaceName;
}
// returns the value of the corresponding text attribute
private static object GetFontSize(NativeMethods.LOGFONT logfont)
{
// note: this assumes integral point sizes. violating this assumption would confuse the user
// because they set something to 7 point but reports that it is, say 7.2 point, due to the rounding.
IntPtr hdc = Misc.GetDC(IntPtr.Zero);
if (hdc == IntPtr.Zero)
{
return null;
}
int lpy = UnsafeNativeMethods.GetDeviceCaps(hdc, NativeMethods.LOGPIXELSY);
Misc.ReleaseDC(IntPtr.Zero, hdc);
return Math.Round((double)(-logfont.lfHeight) * 72 / lpy);
}
// returns the value of the corresponding text attribute
private static object GetFontWeight(NativeMethods.LOGFONT logfont)
{
return logfont.lfWeight;
}
// returns the value of the corresponding text attribute
private static object GetForegroundColor()
{
// NOTE! it is possible for parents of edit controls to change the text color by responding
// to WM_CTLCOLOREDIT however we have decided not to handle that case.
return SafeNativeMethods.GetSysColor(NativeMethods.COLOR_WINDOWTEXT);
}
// returns the value of the corresponding text attribute
private static object GetItalic(NativeMethods.LOGFONT logfont)
{
return logfont.lfItalic != 0;
}
// returns the value of the corresponding text attribute
private static object GetStrikethroughStyle(NativeMethods.LOGFONT logfont)
{
return logfont.lfStrikeOut != 0 ? TextDecorationLineStyle.Single : TextDecorationLineStyle.None;
}
// returns the value of the corresponding text attribute
private static object GetUnderlineStyle(NativeMethods.LOGFONT logfont)
{
return logfont.lfUnderline != 0 ? TextDecorationLineStyle.Single : TextDecorationLineStyle.None;
}
// moves an endpoint forward a certain number of units.
// the endpoint is just an index into the text so it could represent either
// the endpoint.
private int MoveEndpointForward(int index, TextUnit unit, int count, out int moved)
{
switch (unit)
{
case TextUnit.Character:
{
int limit = _provider.GetTextLength() ;
ValidateEndpoints();
moved = Math.Min(count, limit - index);
index = index + moved;
index = index > limit ? limit : index;
}
break;
case TextUnit.Word:
{
string text = _provider.GetText();
ValidateEndpoints();
#if WCP_NLS_ENABLED
// use the same word breaker as Avalon Text.
WordBreaker breaker = new WordBreaker();
TextContainer container = new TextContainer(text);
TextNavigator navigator = new TextNavigator(index, container);
// move forward one word break for each count
for (moved = 0; moved < count && index < text.Length; moved++)
{
if (!breaker.MoveToNextWordBreak(navigator))
break;
}
index = navigator.Position;
#else
for (moved = 0; moved < count && index < text.Length; moved++)
{
for (index++; !AtWordBoundary(text, index); index++) ;
}
#endif
}
break;
case TextUnit.Line:
{
// figure out what line we are on. if we are in the middle of a line and
// are moving left then we'll round up to the next line so that we move
// to the beginning of the current line.
int line = _provider.LineFromChar(index);
// limit the number of lines moved to the number of lines available to move
// Note lineMax is always >= 1.
int lineMax = _provider.GetLineCount();
moved = Math.Min(count, lineMax - line - 1);
if (moved > 0)
{
// move the endpoint to the beginning of the destination line.
index = _provider.LineIndex(line + moved);
}
else if (moved == 0 && lineMax == 1)
{
// There is only one line so get the text length as endpoint
index = _provider.GetTextLength();
moved = 1;
}
}
break;
case TextUnit.Paragraph:
{
// just like moving words but we look for paragraph boundaries instead of
// word boundaries.
string text = _provider.GetText();
ValidateEndpoints();
for (moved = 0; moved < count && index < text.Length; moved++)
{
for (index++; !AtParagraphBoundary(text, index); index++) ;
}
}
break;
case TextUnit.Format:
case TextUnit.Page:
case TextUnit.Document:
{
// since edit controls are plain text moving one uniform format unit will
// take us all the way to the end of the document, just like
// "pages" and document.
int limit = _provider.GetTextLength();
ValidateEndpoints();
// we'll move 1 format unit if we aren't already at the end of the
// document. Otherwise, we won't move at all.
moved = index < limit ? 1 : 0;
index = limit;
}
break;
default:
throw new System.ComponentModel.InvalidEnumArgumentException("unit", (int)unit, typeof(TextUnit));
}
return index;
}
// moves an endpoint backward a certain number of units.
// the endpoint is just an index into the text so it could represent either
// the endpoint.
private int MoveEndpointBackward(int index, TextUnit unit, int count, out int moved)
{
switch (unit)
{
case TextUnit.Character:
{
int limit = _provider.GetTextLength();
ValidateEndpoints();
int oneBasedIndex = index + 1;
moved = Math.Max(count, -oneBasedIndex);
index = index + moved;
index = index < 0 ? 0 : index;
}
break;
case TextUnit.Word:
{
string text = _provider.GetText();
ValidateEndpoints();
#if WCP_NLS_ENABLED
// use the same word breaker as Avalon Text.
WordBreaker breaker = new WordBreaker();
TextContainer container = new TextContainer(text);
TextNavigator navigator = new TextNavigator(index, container);
// move backward one word break for each count
for (moved = 0; moved > count && index > 0; moved--)
{
if (!breaker.MoveToPreviousWordBreak(navigator))
break;
}
index = navigator.Position;
#else
for (moved = 0; moved > count && index > 0; moved--)
{
for (index--; !AtWordBoundary(text, index); index--) ;
}
#endif
}
break;
case TextUnit.Line:
{
// Note count < 0.
// Get 1-based line.
int line = _provider.LineFromChar(index) + 1;
int lineMax = _provider.GetLineCount();
// Truncate the count to the number of available lines.
int actualCount = Math.Max(count, -line);
moved = actualCount;
if (actualCount == -line)
{
// We are moving by the maximum number of possible lines,
// so we know the resulting index will be 0.
index = 0;
// If a line other than the first consists of only "\r\n",
// you can move backwards past this line and the position changes,
// hence this is counted. The first line is special, though:
// if it is empty, and you move say from the second line back up
// to the first, you cannot move further; however if the first line
// is nonempty, you can move from the end of the first line to its
// beginning! This latter move is counted, but if the first line
// is empty, it is not counted.
// Recalculate the value of "moved".
// The first line is empty if it consists only of
// a line separator sequence.
bool firstLineEmpty =
((lineMax > 1 && _provider.LineIndex(1) == _lineSeparator.Length)
|| lineMax == 0);
if (moved < 0 && firstLineEmpty)
{
++moved;
}
}
else // actualCount > -line
{
// Move the endpoint to the beginning of the following line,
// then back by the line separator length to get to the end
// of the previous line, since the Edit control has
// no method to get the character index of the end
// of a line directly.
index = _provider.LineIndex(line + actualCount) - _lineSeparator.Length;
}
}
break;
case TextUnit.Paragraph:
{
// just like moving words but we look for paragraph boundaries instead of
// word boundaries.
string text = _provider.GetText();
ValidateEndpoints();
for (moved = 0; moved > count && index > 0; moved--)
{
for (index--; !AtParagraphBoundary(text, index); index--) ;
}
}
break;
case TextUnit.Format:
case TextUnit.Page:
case TextUnit.Document:
{
// since edit controls are plain text moving one uniform format unit will
// take us all the way to the beginning of the document, just like
// "pages" and document.
// we'll move 1 format unit if we aren't already at the beginning of the
// document. Otherwise, we won't move at all.
moved = index > 0 ? -1 : 0;
index = 0;
}
break;
default:
throw new System.ComponentModel.InvalidEnumArgumentException("unit", (int)unit, typeof(TextUnit));
}
return index;
}
// method to set both endpoints simultaneously
private void MoveTo(int start, int end)
{
if (start < 0 || end < start)
{
throw new InvalidOperationException(SR.Format(SR.InvalidTextRangeOffset, GetType().FullName));
}
_start = start;
_end = end;
}
private void ValidateEndpoints()
{
int limit = _provider.GetTextLength();
if (Start > limit || End > limit)
{
throw new InvalidOperationException(SR.Format(SR.InvalidRangeEndpoint, GetType().FullName));
}
}
#endregion Private Methods
//------------------------------------------------------
//
// Private Properties
//
//------------------------------------------------------
#region Private Properties
private bool IsDegenerate
{
get
{
// strictly only needs to be == since never should _start>_end.
return _start >= _end;
}
}
private int End
{
get
{
return _end;
}
set
{
// ensure that we never accidentally get a negative index
if (value < 0)
{
throw new InvalidOperationException(SR.Format(SR.InvalidTextRangeOffset, GetType().FullName));
}
// ensure that end never moves before start
if (value < _start)
{
_start = value;
}
_end = value;
}
}
private int Length
{
get
{
return _end - _start;
}
}
private int Start
{
get
{
return _start;
}
set
{
// ensure that we never accidentally get a negative index
if (value < 0)
{
throw new InvalidOperationException(SR.Format(SR.InvalidTextRangeOffset, GetType().FullName));
}
// ensure that start never moves after end
if (value > _end)
{
_end = value;
}
_start = value;
}
}
#endregion Private Properties
//------------------------------------------------------
//
// Static Methods
//
//------------------------------------------------------
#region Static Methods
#endregion Static Methods
//------------------------------------------------------
//
// Private Fields
//
//------------------------------------------------------
#region Private Fields
private WindowsEditBox _provider;
private int _start;
private int _end;
// Edit controls always use "\r\n" as the line separator, not "\n".
private const string _lineSeparator = "\r\n"; // This string is a non-localizable string
#endregion Private Fields
}
}
|