File: MS\Internal\AutomationProxies\WindowsRichEdit.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\UIAutomation\UIAutomationClientSideProviders\UIAutomationClientSideProviders.csproj (UIAutomationClientSideProviders)
// 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: HWND-based RichEdit Proxy
 
using System;
using System.Diagnostics;
using System.Text;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Automation.Provider;
using Accessibility;
using MS.Win32;
using NativeMethodsSetLastError = MS.Internal.UIAutomationClientSideProviders.NativeMethodsSetLastError;
 
namespace MS.Internal.AutomationProxies
{
    class WindowsRichEdit : ProxyHwnd, IValueProvider, ITextProvider
    {
        // ------------------------------------------------------
        //
        // Constructors
        //
        // ------------------------------------------------------
 
        #region Constructors
 
        WindowsRichEdit (IntPtr hwnd, ProxyFragment parent, int style)
            : base( hwnd, parent, style )
        {
            _type = WindowsEditBox.GetEditboxtype(hwnd);
            if (IsMultiline)
            {
                _cControlType = ControlType.Document;
            }
            else
            {
                _cControlType = ControlType.Edit;
            }
 
            _fIsKeyboardFocusable = true;
 
            // support for events
            _createOnEvent = new WinEventTracker.ProxyRaiseEvents (RaiseEvents);
        }
 
        #endregion Initialization
 
        #region Proxy Create
 
        // Static Create method called by UIAutomation to create this proxy.
        // returns null if unsuccessful
        internal static IRawElementProviderSimple Create(IntPtr hwnd, int idChild, int idObject)
        {
            return Create(hwnd, idChild);
        }
 
        internal static IRawElementProviderSimple Create(IntPtr hwnd, int idChild)
        {
            // Something is wrong if idChild is not zero
            ArgumentOutOfRangeException.ThrowIfNotEqual(idChild, 0);
 
            return new WindowsRichEdit(hwnd, null, 0);
        }
 
        // Static Create method called by the event tracker system
        internal static void RaiseEvents (IntPtr hwnd, int eventId, object idProp, int idObject, int idChild)
        {
            if (idObject != NativeMethods.OBJID_VSCROLL && idObject != NativeMethods.OBJID_HSCROLL)
            {
                WindowsRichEdit wtv = new WindowsRichEdit (hwnd, null, 0);
 
                // If this event means the selection may have changed, raise that event
                // here instead of relying on the generic DispatchEvents mechanism below.
                // The RichTextEdit provider needs to handle this event specially.
                if (eventId == NativeMethods.EventObjectLocationChange
                    && idObject == NativeMethods.OBJID_CARET)
                {
                    wtv.RaiseTextSelectionEvent(eventId, idProp, idObject, idChild);
                }
                else
                {
                    wtv.DispatchEvents(eventId, idProp, idObject, idChild);
                }
            }
        }
 
        #endregion Proxy Create
 
        //------------------------------------------------------
        //
        //  Patterns Implementation
        //
        //------------------------------------------------------
 
        #region ProxySimple Interface
 
        // Returns a pattern interface if supported.
        internal override object GetPatternProvider(AutomationPattern iid)
        {
            try
            {
                if (iid == ValuePattern.Pattern && !IsMultiline)
                {
                    if (IsDocument())
                    {
                        EnsureTextDocument();
                    }
                    return this;
                }
 
                // text pattern is supported for non-password boxes
                if (iid == TextPattern.Pattern && _type != WindowsEditBox.EditboxType.Password && IsDocument())
                {
                    EnsureTextDocument();
                    return this;
                }
 
                return null;
            }
                //If tom.dll is missing
            catch (System.IO.FileNotFoundException)
            {
                return null;
            }
        }
 
        // Process all the Logical and Raw Element Properties
        internal override object GetElementProperty(AutomationProperty idProp)
        {
            if (idProp == AutomationElement.IsPasswordProperty)
            {
                return _type == WindowsEditBox.EditboxType.Password;
            }
            else if (idProp == AutomationElement.NameProperty)
            {
                // Per ControlType.Edit spec if there is no static text
                // label for an Edit then it must NEVER return the contents
                // of the edit as the Name property. This relies on the
                // default ProxyHwnd impl looking for a label for Name and
                // not using WM_GETTEXT on the edit hwnd.
                string name = base.GetElementProperty(idProp) as string;
                if (string.IsNullOrEmpty(name))
                {
                    // Stop UIA from asking other providers
                    return AutomationElement.NotSupported;
                }
 
                return name;
            }
 
            return base.GetElementProperty(idProp);
        }
        #endregion ProxySimple Interface
 
        #region Value Pattern
 
        // Sets the text of the edit.
        void IValueProvider.SetValue(string str)
        {
            // Check if the window is disabled
            if (!SafeNativeMethods.IsWindowEnabled(_hwnd))
            {
                throw new ElementNotEnabledException();
            }
 
            if (Misc.IsBitSet(WindowStyle, NativeMethods.ES_READONLY))
            {
                throw new InvalidOperationException(SR.ValueReadonly);
            }
 
            // check if control only accepts numbers
            if (Misc.IsBitSet(WindowStyle, NativeMethods.ES_NUMBER) && !WindowsFormsHelper.IsWindowsFormsControl(_hwnd))
            {
                // check if string contains any non-numeric characters.
                foreach (char ch in str)
                {
                    if (char.IsLetter(ch))
                    {
                        throw new ArgumentException(SR.Format(SR.NotAValidValue, str), "val");
                    }
                }
            }
 
            // Text/edit box should not enter more characters than what is allowed through keyboard.
            // Determine the max number of chars this editbox accepts
 
            int result = Misc.ProxySendMessageInt(_hwnd, NativeMethods.EM_GETLIMITTEXT, IntPtr.Zero, IntPtr.Zero);
            if (result < str.Length)
            {
                throw new InvalidOperationException (SR.OperationCannotBePerformed);
            }
 
            // Send the message...
            result = Misc.ProxySendMessageInt(_hwnd, NativeMethods.WM_SETTEXT, IntPtr.Zero, new StringBuilder(str));
            if (result != 1)
            {
                throw new InvalidOperationException(SR.OperationCannotBePerformed);
            }
        }
 
        // Request to get the value that this UI element is representing as a string
        string IValueProvider.Value
        {
            get
            {
                return GetValue();
            }
        }
 
        bool IValueProvider.IsReadOnly
        {
            get
            {
                return IsReadOnly();
            }
        }
 
        #endregion
 
        #region Text Pattern Methods
 
        ITextRangeProvider [] ITextProvider.GetSelection()
        {
            // we must have called EnsureTextDocument() before arriving here.
            Debug.Assert(_document != null);
 
            // clone a range from the documents selection
            ITextRange range = null;
            ITextSelection selection = _document.Selection;
            if (selection != null)
            {
                // duplicate the selection range since we don't want their modifications to affect the selection
                range = selection.GetDuplicate();
 
                // for future reference: active endpoint is
                // ((selection.Flags & TomSelectionFlags.tomSelStartActive) == TomSelectionFlags.tomSelStartActive) ? TextPatternRangeEndpoint.Start : TextPatternRangeEndpoint.End;
            }
 
            if (range == null)
                return new ITextRangeProvider[] { };
            else
                return new ITextRangeProvider[] { new WindowsRichEditRange(range, this) };
        }
 
        ITextRangeProvider [] ITextProvider.GetVisibleRanges()
        {
            ITextRange range = GetVisibleRange();
 
            if (range == null)
                return new ITextRangeProvider[] { };
            else
                return new ITextRangeProvider[] { new WindowsRichEditRange(range, this) };
        }
 
        ITextRangeProvider ITextProvider.RangeFromChild(IRawElementProviderSimple childElement)
        {
            // we don't have any children so this call must be in error.
            // if we implement children for hyperlinks and embedded objects then we'll need to change this.
            throw new InvalidOperationException(SR.Format(SR.RichEditTextPatternHasNoChildren, GetType().FullName));
        }
 
        ITextRangeProvider ITextProvider.RangeFromPoint(Point screenLocation)
        {
            // we must have called EnsureTextDocument() before arriving here.
            Debug.Assert(_document != null);
 
            // TextPattern has verified that the point is inside our client area so we don't need to check for that.
 
            // get the degenerate range at the point
            // we're assuming ITextDocument::RangeFromPoint always returns a degenerate range
            ITextRange range = _document.RangeFromPoint((int)screenLocation.X, (int)screenLocation.Y);
            Debug.Assert(range.Start == range.End);
 
            // if you wanted to get the character under the point instead of the degenerate range nearest
            // the point, then you would add:
 
            //// if the point is within the character to the right then expand the degenerate range to
            //// include the character.
            //Rect rect;
            //range.MoveEnd(TomUnit.tomCharacter, 1);
            //rect = WindowsRichEditRange.GetSingleLineRangeRectangle(range, BoundingRectangle);
            //if (!rect.Contains(screenLocation))
            //{
            //    // if the point is within the character to the left then expand the degenerate range
            //    // to include the character.
            //    range.Collapse(TomStartEnd.tomStart);
            //    range.MoveStart(TomUnit.tomCharacter, -1);
            //    rect = WindowsRichEditRange.GetSingleLineRangeRectangle(range, BoundingRectangle);
            //    if (!rect.Contains(screenLocation))
            //    {
            //        // the point is not in an adjacent character cell so leave it degenerate.
            //        range.Collapse(TomStartEnd.tomEnd);
            //    }
            //}
 
            return range != null ? new WindowsRichEditRange(range, this) : null;
        }
 
        #endregion TextPattern Methods
 
        #region TextPattern Properties
 
        ITextRangeProvider ITextProvider.DocumentRange
        {
            get
            {
                // we must have called EnsureTextDocument() before arriving here.
                Debug.Assert(_document != null);
 
                // create a text range that covers the entire main story.
                ITextRange range = _document.Range(0, 0);
                range.SetRange(0, range.StoryLength);
                return new WindowsRichEditRange(range, this);
            }
        }
 
      
        SupportedTextSelection ITextProvider.SupportedTextSelection
        {
            get
            {
                return SupportedTextSelection.Single;
            }
        }
 
        #endregion Text Properties
 
        //------------------------------------------------------
        //
        //  Internal Methods
        //
        //------------------------------------------------------
 
        #region Internal Methods
 
        // returns the range that is visible in the RichEdit window
        internal ITextRange GetVisibleRange()
        {
            // get a range from the center point of the client rectangle
            Rect rect = BoundingRectangle;
            int x = ((int)rect.Left + (int)rect.Right) / 2;
            int y = ((int)rect.Top + (int)rect.Bottom) / 2;
            ITextRange range = _document.RangeFromPoint(x, y);
 
            // expand it to fill the window.
            range.Expand(TomUnit.tomWindow);
 
            // There is a bug with RichEdit 3.0.  The expand to tomWindow may gets 0 as the range's cpBegin (Start).
            // So need to trim off what is outside of the window.
            int start = range.Start;
            // The ITextRange::SetRange method sets this range's Start = min(cp1, cp2) and End = max(cp1, cp2).
            // If the range is a nondegenerate selection, cp2 is the active end; if it's a degenerate selection,
            // the ambiguous cp is displayed at the start of the line (rather than at the end of the previous line).
            // Set the end to the start and the start to the end to create an ambiguous cp.
            range.SetRange(range.End, range.Start);
            bool gotPoint = WindowsRichEditRange.RangeGetPoint(range, TomGetPoint.tomStart, out x, out y);
            while (!gotPoint || !Misc.PtInRect(ref rect, x, y))
            {
                range.MoveStart(TomUnit.tomWord, 1);
                gotPoint = WindowsRichEditRange.RangeGetPoint(range, TomGetPoint.tomStart, out x, out y);
            }
 
            if (start != range.Start)
            {
                // The trimming was done based on the left edge of the range.  The last visiable partial
                // character/word has been also added back into the range, need to remove it.  Do the comparing
                // against the characters right edge and the window rectangle.
                ITextRange rangeAdjust = _document.Range(0, range.Start - 1);
                gotPoint = WindowsRichEditRange.RangeGetPoint(rangeAdjust, TomGetPoint.TA_BOTTOM | TomGetPoint.TA_RIGHT, out x, out y);
 
                while (gotPoint && Misc.PtInRect(ref rect, x, y) && rangeAdjust.Start != rangeAdjust.End)
                {
                    rangeAdjust.MoveEnd(TomUnit.tomCharacter, -1);
                    range.MoveStart(TomUnit.tomCharacter, -1);
                    gotPoint = WindowsRichEditRange.RangeGetPoint(rangeAdjust, TomGetPoint.TA_BOTTOM | TomGetPoint.TA_RIGHT, out x, out y);
                }
            }
 
            // There is a bug with RichEdit 3.0.  The expand to tomWindow gets the last cp of the bottom
            // line in the window as the range's cpLim (End).  The cpLim may be passed the right side of
            // the window.
            // So need to trim off what is on the right side of the window.
            int end = range.End;
            gotPoint = WindowsRichEditRange.RangeGetPoint(range, TomGetPoint.TA_RIGHT, out x, out y);
            while (!gotPoint || !Misc.PtInRect(ref rect, x, y))
            {
                range.MoveEnd(TomUnit.tomWord, -1);
                gotPoint = WindowsRichEditRange.RangeGetPoint(range, TomGetPoint.TA_RIGHT, out x, out y);
            }
 
            if (end != range.End)
            {
                // The trimming was done based on the right edge of the range.  The last visiable partial
                // character/word has been also trimmed so add it back to the range.  Do the comparing
                // against the characters left edge and the window rectangle.
                ITextRange rangeAdjust = _document.Range(range.End, end);
                do
                {
                    if (range.MoveEnd(TomUnit.tomCharacter, 1) == 0)
                    {
                        break;
                    }
                    rangeAdjust.MoveStart(TomUnit.tomCharacter, 1);
                    gotPoint = WindowsRichEditRange.RangeGetPoint(rangeAdjust, TomGetPoint.tomStart, out x, out y);
                } while (gotPoint && Misc.PtInRect(ref rect, x, y));
            }
 
            return range;
        }
 
        #endregion Internal Methods
 
        //------------------------------------------------------
        //
        //  Internal Properties
        //
        //------------------------------------------------------
 
        #region Internal Properties
 
        internal ITextDocument Document
        {
            get
            {
                return _document;
            }
        }
 
        internal bool IsMultiline
        {
            get
            {
                return _type == WindowsEditBox.EditboxType.Multiline;
            }
        }
 
        internal bool ReadOnly
        {
            get
            {
                return (WindowStyle & NativeMethods.ES_READONLY) == NativeMethods.ES_READONLY;
            }
        }
 
        #endregion Internal Properties
 
        //------------------------------------------------------
        //
        //  Internal Methods
        //
        //------------------------------------------------------
 
        #region Internal Methods
 
        // return true if the user can modify the text in the edit control.
        internal bool IsReadOnly()
        {
            return (!SafeNativeMethods.IsWindowEnabled(WindowHandle) ||
                    Misc.IsBitSet(WindowStyle, NativeMethods.ES_READONLY));
        }
 
        #endregion
 
        //------------------------------------------------------
        //
        //  Private Methods
        //
        //------------------------------------------------------
 
        #region Private Methods
 
        // call this method before allowing access to any TextPattern properties or methods.
        // it ensures that we have an ITextDocument pointer to the richedit control.
        private void EnsureTextDocument()
        {
            // if we don't have a document pointer yet then get one by sending richedit a WM_GETOBJECT message
            // with object id asking for the native OM.
            if (_document == null)
            {
                object obj = null;
 
                if (UnsafeNativeMethods.AccessibleObjectFromWindow(WindowHandle, NativeMethods.OBJID_NATIVEOM, ref UnsafeNativeMethods.IID_IDispatch, ref obj) != NativeMethods.S_OK)
                {
                    throw new System.NotImplementedException(SR.NoITextDocumentFromRichEdit);
                }
 
                // This is temp solution which will prevent exception in the case
                // when we cannot obtain the ITextDocument from a RichEdit control
                // The direct cast does not work for RichEdit20w controls used in MS Office
                _document = obj as ITextDocument;
                if (_document == null)
                {
                    throw new System.NotImplementedException(SR.NoITextDocumentFromRichEdit);
                }
            }
        }
 
        private string GetValue()
        {
            if (_type == WindowsEditBox.EditboxType.Password)
            {
                // Trying to retrieve the text (through WM_GETTEXT) from an edit box that has
                // the ES_PASSWORD style throws an UnauthorizedAccessException.
                throw new UnauthorizedAccessException();
            }
 
            if (IsDocument())
            {
                ITextRange range = _document.Range(0, 0);
                int start = 0;
                int end = range.StoryLength;
 
                range.SetRange(start, end);
 
                string text = range.Text;
                // Empty edits contain a degenerate/empty range, and will return null
                // for their text - treat this as "", not null, since we do want to expose
                // a non-null value.
                if (string.IsNullOrEmpty(text))
                {
                    return "";
                }
 
                int embeddedObjectOffset = text.IndexOf((char)0xFFFC);
                if (embeddedObjectOffset != -1)
                {
                    StringBuilder sbText = new StringBuilder();
                    object embeddedObject;
                    while (start < end && embeddedObjectOffset != -1)
                    {
                        sbText.Append(text.Substring(start, embeddedObjectOffset - start));
                        range.SetRange(embeddedObjectOffset, end);
                        if (range.GetEmbeddedObject(out embeddedObject) == NativeMethods.S_OK && embeddedObject != null)
                        {
                            GetEmbeddedObjectText(embeddedObject, sbText);
                        }
                        else
                        {
                            // If there is some kind of error, just append a space to the text.  In this way
                            // we will be no worse of then before implementing the embedded object get text.
                            sbText.Append(' ');
                        }
                        start = embeddedObjectOffset + 1;
                        embeddedObjectOffset = text.IndexOf((char)0xFFFC, start);
                    }
 
                    if (start < end)
                    {
                        sbText.Append(text.Substring(start, end - start));
                    }
 
                    text = sbText.ToString();
                }
 
                return text;
            }
            else
            {
                return Misc.ProxyGetText(_hwnd);
            }
        }
 
        private void GetEmbeddedObjectText(object embeddedObject, StringBuilder sbText)
        {
            string text;
 
            IAccessible acc = embeddedObject as IAccessible;
            if (acc != null)
            {
                text = acc.get_accName(NativeMethods.CHILD_SELF);
                if (!string.IsNullOrEmpty(text))
                {
                    sbText.Append(text);
                    return;
                }
            }
 
            // Didn't get IAccessible (or didn't get a name from it).
            // Try the IDataObject technique instead...
 
            int hr = NativeMethods.S_FALSE;
            IDataObject dataObject = null;
            IOleObject oleObject = embeddedObject as IOleObject;
            if (oleObject != null)
            {
                // Try IOleObject::GetClipboardData (which returns an IDataObject) first...
                hr = oleObject.GetClipboardData(0, out dataObject);
            }
 
            // If that didn't work, try the embeddedObject as a IDataObject instead...
            if (hr != NativeMethods.S_OK)
            {
                dataObject = embeddedObject as IDataObject;
            }
 
            if (dataObject == null)
            {
                return;
            }
 
            // Got the IDataObject. Now query it for text formats. Try Unicode first...
 
            bool fGotUnicode = true;
 
            UnsafeNativeMethods.FORMATETC fetc = new UnsafeNativeMethods.FORMATETC();
            fetc.cfFormat = DataObjectConstants.CF_UNICODETEXT;
            fetc.ptd = IntPtr.Zero;
            fetc.dwAspect = DataObjectConstants.DVASPECT_CONTENT;
            fetc.lindex = -1;
            fetc.tymed = DataObjectConstants.TYMED_HGLOBAL;
 
            UnsafeNativeMethods.STGMEDIUM med = new UnsafeNativeMethods.STGMEDIUM();
            med.tymed = DataObjectConstants.TYMED_HGLOBAL;
            med.pUnkForRelease = IntPtr.Zero;
            med.hGlobal = IntPtr.Zero;
 
            hr = dataObject.GetData(ref fetc, ref med);
 
            if (hr != NativeMethods.S_OK || med.hGlobal == IntPtr.Zero)
            {
                // If we didn't get Unicode, try for ANSI instead...
                fGotUnicode = false;
                fetc.cfFormat = DataObjectConstants.CF_TEXT;
 
                hr = dataObject.GetData(ref fetc, ref med);
            }
 
            // Did we get anything?
            if (hr != NativeMethods.S_OK || med.hGlobal == IntPtr.Zero)
            {
                return;
            }
 
            //lock the memory, so data can be copied into
            IntPtr globalMem = UnsafeNativeMethods.GlobalLock(med.hGlobal);
 
            try
            {
                //error check for the memory pointer
                if (globalMem == IntPtr.Zero)
                {
                    return;
                }
 
                unsafe
                {
                    //get the string
                    if (fGotUnicode)
                    {
                        text = new string((char*)globalMem);
                    }
                    else
                    {
                        text = new string((sbyte*)globalMem);
                    }
                }
 
                sbText.Append(text);
            }
            finally
            {
                //unlock the memory
                UnsafeNativeMethods.GlobalUnlock(med.hGlobal);
                UnsafeNativeMethods.ReleaseStgMedium(ref med);
            }
        }
 
        private bool IsDocument()
        {
            return !OnCommandBar();
        }
 
        private bool OnCommandBar()
        {
            IntPtr hwndParent = NativeMethodsSetLastError.GetAncestor(_hwnd, NativeMethods.GA_PARENT);
 
            if (hwndParent != IntPtr.Zero)
            {
                return Misc.GetClassName(hwndParent).Equals("MsoCommandBar");
            }
            return false;
        }
 
        // Raise the TextSelectionChanged event, if appropriate.
        private void RaiseTextSelectionEvent(int eventId, object idProp, int idObject, int idChild)
        {
            // We cannot use the generic DispatchEvents mechanism to raise
            // the TextSelection event, since DispatchEvents() will eventually call
            // EventManager.HandleTextSelectionChangedEvent(),
            // which will use logic inappropriate for the RichTextEdit control:
            // namely, that wtv._document.Selection.GetSelection() returns
            // an ITextRange which can *change* when e.g. backspace is pressed
            // in the control.  To reliably detect an actual change in selection,
            // we have to perform a specific test of the old and new selection
            // endpoints and raise the event ourselves if appropriate.
 
            bool raiseTextSelectionEvent = false;
 
            EnsureTextDocument();
            ITextSelection textSelection = _document.Selection;
            if (textSelection != null)
            {
                raiseTextSelectionEvent =
                    (_raiseEventsOldSelectionStart != textSelection.Start
                        || _raiseEventsOldSelectionEnd != textSelection.End);
 
                _raiseEventsOldSelectionStart = textSelection.Start;
                _raiseEventsOldSelectionEnd = textSelection.End;
            }
            else
            {
                raiseTextSelectionEvent =
                    (_raiseEventsOldSelectionStart != _NO_ENDPOINT
                        || _raiseEventsOldSelectionEnd != _NO_ENDPOINT);
 
                _raiseEventsOldSelectionStart = _NO_ENDPOINT;
                _raiseEventsOldSelectionEnd = _NO_ENDPOINT;
            }
 
            if (raiseTextSelectionEvent)
            {
                AutomationInteropProvider.RaiseAutomationEvent(
                    TextPattern.TextSelectionChangedEvent,
                    this, new AutomationEventArgs(TextPattern.TextSelectionChangedEvent));
            }
        }
 
        #endregion private Methods
 
        //------------------------------------------------------
        //
        //  Private Fields
        //
        //------------------------------------------------------
 
        #region Private Fields
 
        private ITextDocument _document;
        private WindowsEditBox.EditboxType _type;
 
        // Used in RaiseEvents() to track changes in the selection endpoints.
        static private int _raiseEventsOldSelectionStart;
        static private int _raiseEventsOldSelectionEnd;
        private const int _NO_ENDPOINT = -1;
 
        #endregion private Fields
    }
}