File: MS\Internal\AutomationProxies\ClickablePoint.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.
 
using System;
using System.Collections;
using MS.Win32;
 
// PRESHARP: In order to avoid generating warnings about unkown message numbers and unknown pragmas.
#pragma warning disable 1634, 1691
 
namespace MS.Internal.AutomationProxies
{
    static internal class ClickablePoint
    {
        /// <summary>
        /// Static Constructor. Retrieve and keeps the hwnd for "Program"
        /// The Windows Rectangle for "Program"  is the union for the real
        /// Estate for all the monitors.
        /// </summary>
        static ClickablePoint()
        {
            _hwndProgman = Misc.FindWindowEx(IntPtr.Zero, IntPtr.Zero, "Progman", null);
            if (_hwndProgman == IntPtr.Zero)
            {
                _hwndProgman = _hwndDesktop;
            }
        }
        
        //------------------------------------------------------
        //
        //  Internal Methods
        //
        //------------------------------------------------------
 
        #region Internal Methods
 
        /// <summary>
        /// Return a clickable point in a Rectangle for a given window
        ///
        /// The algorithm uses the Windows Z order.
        /// To be visible, a rectangle must be enclosed in the hwnd of its parents 
        /// and must but at least partially on top of the all of siblings as predecessors.
        /// In the windows ordered scheme, the first sibling comes on top followed by the 
        /// next, etc. For a given hwnd, it is sufficent then to look for all the 
        /// predecessor siblings.
        ///
        /// The scheme below is using recursion. This is make slightly harder to read
        /// But makes it a bit more efficent.
        /// </summary>
        /// <param name="hwnd">Window Handle</param>
        /// <param name="alIn">Input list of Rectangles to check GetPoint against</param>
        /// <param name="alOut">Output list of Rectangles after the exclusion test</param>
        /// <param name="pt">Clickable Point</param>
        /// <returns>True if there is a clickable in ro</returns>
        static internal bool GetPoint(IntPtr hwnd, ArrayList alIn, ArrayList alOut, ref NativeMethods.Win32Point pt)
        {
            IntPtr hwndStart = hwnd;
            IntPtr hwndCurrent = hwnd;
 
            // Do the window on top exclusion
            // Only one level deep is necessary as grand children are clipped to their parent (our children)
            for (hwnd = Misc.GetWindow(hwnd, NativeMethods.GW_CHILD); hwnd != IntPtr.Zero; hwnd = Misc.GetWindow(hwnd, NativeMethods.GW_HWNDNEXT))
            {
                // For siblings, the element bounding rectangle must not be covered by the
                // bounding rect of its siblings
                if (!ClickableInRect(hwnd, ref pt, true, alIn, alOut))
                {
                    return false;
                }
            }
 
            // Check for Parent and Sibling
            hwnd = hwndStart;
            while (true)
            {
                hwnd = Misc.GetWindow(hwnd, NativeMethods.GW_HWNDPREV);
                if (hwnd == IntPtr.Zero)
                {
                    // Very top of the Windows hierarchy we're done
                    if (hwndCurrent == _hwndDesktop)
                    {
                        break;
                    }
 
                    // The desktop is the parent we should stop here
                    if (Misc.IsBitSet(Misc.GetWindowStyle(hwndCurrent), NativeMethods.WS_POPUP))
                    {
                        hwnd = _hwndDesktop;
                    }
                    else
                    {
                        // We finished with all the hwnd siblings so get to the parent
                        hwnd = Misc.GetParent(hwndCurrent);
                    }
 
                    if (hwnd == IntPtr.Zero)
                    {
                        // final clipping against the desktop
                        hwnd = _hwndDesktop;
                    }
 
                    // For parent, the element bounding rectangle must be within it's parent bounding rect
                    // The desktop contains the bounding rectangle only for the main monintor, the 
                    // The Progman window contains the area for the union of all the monitors
                    // Substitute the Desktop with the Progman hwnd for clipping calculation
                    IntPtr hwndClip = hwnd == _hwndDesktop ? _hwndProgman : hwnd;
 
                    if (!ClickableInRect(hwndClip, ref pt, false, alIn, alOut))
                    {
                        return false;
                    }
 
                    // Current Parent
                    hwndCurrent = hwnd;
                    continue;
                }
 
                // For siblings, the element bounding rectangle must not be covered by the
                // bounding rect of its siblings
                if (!ClickableInRect(hwnd, ref pt, true, alIn, alOut))
                {
                    return false;
                }
            }
 
            return true;
        }
 
        /// <summary>
        /// Go through the list of all element chidren and exclude them from the list of 
        /// visible/clickable rectangles.
        /// The element children may be listed in any order. A check on all of them must
        /// be performed. There is no easy way out.
        /// </summary>
        /// <param name="fragment"></param>
        /// <param name="alIn"></param>
        /// <param name="alOut"></param>
        internal static void ExcludeChildren(ProxyFragment fragment, ArrayList alIn, ArrayList alOut)
        {
            // First go through all the children to exclude whatever is on top
            for (ProxySimple simple = fragment.GetFirstChild(); simple != null; simple = fragment.GetNextSibling(simple))
            {
                // The exclusion for hwnd children is done by the GetPoint routine
                if (simple is ProxyHwnd)
                {
                    continue;
                }
 
                // Copy the output bits
                alIn.Clear();
                alIn.AddRange(alOut);
 
                NativeMethods.Win32Rect rc = new NativeMethods.Win32Rect(simple.BoundingRectangle);
                CPRect rcp = new CPRect(ref rc, false);
 
                ClickablePoint.SplitRect(alIn, ref rcp, alOut, true);
 
                // recurse on the children
                if (simple is ProxyFragment)
                {
                    ExcludeChildren((ProxyFragment)simple, alIn, alOut);
                }
            }
        }
 
        #endregion
 
        // ------------------------------------------------------
        //
        // Internal Fields
        //
        // ------------------------------------------------------
 
        #region Internal Fields
 
        /// <summary>
        /// Rectangle is inclusive exclusive
        /// </summary>
        internal struct CPRect
        {
            internal bool _fNotCovered;
 
            internal int _left;
 
            internal int _top;
 
            internal int _right;
 
            internal int _bottom;
 
            internal CPRect(int left, int top, int right, int bottom, bool fRiAsInsideRect)
            {
                _left = left;
                _top = top;
                _right = right;
                _bottom = bottom;
                _fNotCovered = fRiAsInsideRect;
            }
 
            // ref to make it a pointer
            internal CPRect(ref NativeMethods.Win32Rect rc, bool fRiAsInsideRect)
            {
                _left = rc.left;
                _top = rc.top;
                _right = rc.right;
                _bottom = rc.bottom;
                _fNotCovered = fRiAsInsideRect;
            }
 
            // return true if the 2 rectangle intersects
            internal bool Intersect(ref CPRect ri)
            {
                return !(_top >= ri._bottom || ri._top >= _bottom || _left >= ri._right || ri._left >= _right);
            }
 
            // return true if ri completely covers this
            internal bool Overlap(ref CPRect ri)
            {
                return (ri._left <= _left && ri._right >= _right && ri._top <= _top && ri._bottom >= _bottom);
            }
        }
 
        #endregion
 
        // ------------------------------------------------------
        //
        // Private Methods
        //
        // ------------------------------------------------------
 
        #region Private Methods
 
        private static bool ClickableInRect(IntPtr hwnd, ref NativeMethods.Win32Point pt, bool fRiAsInsideRect, ArrayList alIn, ArrayList alOut)
        {
            if (!SafeNativeMethods.IsWindowVisible(hwnd))
            {
                return fRiAsInsideRect;
            }
 
            // Get the window rect. If this window has a width and it is effectivly invisible
            NativeMethods.Win32Rect rc = new NativeMethods.Win32Rect();
 
            if (!Misc.GetWindowRect(hwnd, ref rc))
            {
                return fRiAsInsideRect;
            }
 
            if ((rc.right - rc.left) <= 0 || (rc.bottom - rc.top) <= 0)
            {
                return fRiAsInsideRect;
            }
 
            // Try for transparency...
            if (fRiAsInsideRect)
            {
                int x = (rc.right + rc.left) / 2;
                int y = (rc.top + rc.bottom) / 2;
 
                try
                {
                    int lr = Misc.ProxySendMessageInt(hwnd, NativeMethods.WM_NCHITTEST, IntPtr.Zero, NativeMethods.Util.MAKELPARAM(x, y));
 
                    if (lr == NativeMethods.HTTRANSPARENT)
                    {
                        return true;
                    }
                }
// PRESHARP: Warning - Catch statements should not have empty bodies
#pragma warning disable 6502
                catch (TimeoutException)
                {
                    // Ignore this timeout error.  Avalon HwndWrappers have a problem with this WM_NCHITTEST call sometimes.
                }
#pragma warning restore 6502
            }
 
            // Copy the output bits
            alIn.Clear();
            alIn.AddRange(alOut);
 
            CPRect rcp = new CPRect(ref rc, false);
 
            ClickablePoint.SplitRect(alIn, ref rcp, alOut, fRiAsInsideRect);
            if (!GetClickablePoint(alOut, out pt.x, out pt.y))
            {
                return false;
            }
 
            return true;
        }
 
        /// <summary>
        /// Split a rectangle into a maximum of 3 rectangble.
        ///   ro is the outside rectangle and ri is the inside rectangle
        ///   ro might is split vertically in a a maximim of 3 rectangles sharing the same 
        ///   right and left margin
        /// </summary>
        /// <param name="ro">Outside Rectangle</param>
        /// <param name="ri">Inside Rectangle</param>
        /// <param name="left">Left Margin for the resulting rectangles</param>
        /// <param name="right">Right Margin for the resulting rectangles</param>
        /// <param name="alRect">Array of resulting rectangles</param>
        /// <param name="fRiAsInsideRect">Covered flag</param>
        static private void SplitVertical(ref CPRect ro, ref CPRect ri, int left, int right, ArrayList alRect, bool fRiAsInsideRect)
        {
            // bottom clip
            if (ri._bottom > ro._bottom)
            {
                ri._bottom = ro._bottom;
            }
 
            int top = ro._top;
            int bottom = ri._top;
 
            if (bottom > top)
            {
                alRect.Add(new CPRect(left, top, right, bottom, ro._fNotCovered));
                top = bottom;
            }
 
            bottom = ri._bottom;
            if (bottom > top)
            {
                alRect.Add(new CPRect(left, top, right, bottom, ro._fNotCovered & fRiAsInsideRect));
                top = bottom;
            }
 
            bottom = ro._bottom;
            if (bottom > top)
            {
                alRect.Add(new CPRect(left, top, right, bottom, ro._fNotCovered));
            }
        }
 
        /// <summary>
        /// Slip a rectangle into a maximum of 5 pieces. 
        /// ro is the out rectangle and ri is the exclusion rectangle.
        /// The number of resulting rectangles varies based on the position of ri relative to ro 
        /// Each resulting reactangles are flaged  as covered or not.
        /// The ro covered flag is or'ed to allow for recursive calls
        ///
        ///      +-----------------+
        ///      |     :  2 :      |
        ///      |     :    :      |
        ///      |     ######      |
        ///      |     ###3##      |
        ///      | 1   ######  5   |
        ///      |     ######      |
        ///      |     :    :      |
        ///      |     :  4 :      |
        ///      |     :    :      |
        ///      +-----------------+
        /// </summary>
        /// <param name="ro">Outside Rectangle</param>
        /// <param name="ri">Inside Rectangle</param>
        /// <param name="alRect">Collection of resulting rectangles</param>
        /// <param name="fRiAsInsideRect"></param>
        static private void SplitRect(ref CPRect ro, CPRect ri, ArrayList alRect, bool fRiAsInsideRect)
        {
            // If ri is fully outside easy way out.
            if (!ro._fNotCovered || !ro.Intersect(ref ri))
            {
                ro._fNotCovered &= fRiAsInsideRect;
                alRect.Add(ro);
                return;
            }
 
            if (ro.Overlap(ref ri))
            {
                ro._fNotCovered &= !fRiAsInsideRect;
                alRect.Add(ro);
                return;
            }
 
            // right clip
            if (ri._right > ro._right)
            {
                ri._right = ro._right;
            }
 
            // bottom clip
            if (ri._bottom > ro._bottom)
            {
                ri._bottom = ro._bottom;
            }
 
            int left = ro._left;
            int right = ri._left;
 
            if (right > left)
            {
                alRect.Add(new CPRect(left, ro._top, right, ro._bottom, ro._fNotCovered & fRiAsInsideRect));
                left = right;
            }
 
            right = ri._right;
            if (right > left)
            {
                SplitVertical(ref ro, ref ri, left, right, alRect, !fRiAsInsideRect);
                left = right;
            }
 
            right = ro._right;
            if (right > left)
            {
                alRect.Add(new CPRect(left, ro._top, right, ro._bottom, ro._fNotCovered & fRiAsInsideRect));
            }
        }
 
        /// <summary>
        /// Takes as input a set of rectangles to perform a rectangular decomposition 
        /// based on the ri. It creates a new set of rectangles each of them being 
        /// marked as covered or not.
        /// 
        /// </summary>
        /// <param name="alIn">List of input rectangle</param>
        /// <param name="ri">Overlapping Rectangle</param>
        /// <param name="alOut">New sets of reactangle</param>
        /// <param name="fRiAsInsideRect">Input Rectangle is rectangle covering alIn Rects or everything
        ///                               outside of ri must be marked as covered</param>
        static private void SplitRect(ArrayList alIn, ref CPRect ri, ArrayList alOut, bool fRiAsInsideRect)
        {
            alOut.Clear();
            for (int i = 0, c = alIn.Count; i < c; i++)
            {
                CPRect ro = (CPRect)alIn[i];
 
                SplitRect(ref ro, ri, alOut, fRiAsInsideRect);
            }
        }
 
        /// <summary>
        /// Find a clickable point in a list of rectangle.
        /// Goes through the list of rectangle, stops on the first rectangle that is not covered
        /// and returns the mid point
        /// </summary>
        /// <param name="al">list of ractangle</param>
        /// <param name="x">X coordinate for a clickable point</param>
        /// <param name="y">Y coordinate for a clickable point</param>
        /// <returns>Clickable point found</returns>
        static private bool GetClickablePoint(ArrayList al, out int x, out int y)
        {
            for (int i = 0, c = al.Count; i < c; i++)
            {
                CPRect r = (CPRect)al[i];
 
                if (r._fNotCovered == true && (r._right - r._left) * (r._bottom - r._top) > 0)
                {
                    // Skip if the rectangle is empty
                    if (r._right > r._left && r._bottom > r._top)
                    {
                        // mid point rounded to the left
                        x = ((r._right - 1) + r._left) / 2;
                        y = ((r._bottom - 1) + r._top) / 2;
                        return true;
                    }
                }
            }
 
            x = y = 0;
            return false;
        }
 
        #endregion
        
        #region Private fields
        
        // Top level Desktop window
        private static IntPtr _hwndDesktop = UnsafeNativeMethods.GetDesktopWindow();
 
        /// The WindowsRect for "Program" is the union for the real
        /// estate for all the monitors. Instead of doing clipping against the root of the hwnd
        /// tree that is the desktop. The last clipping should be done against the Progman hwnd.
        private static IntPtr _hwndProgman;
        
        #endregion Private fields
 
    }
}