File: MS\Internal\Automation\ClickablePoint.cs
Project: wpf\src\UIAutomation\UIAutomationClient\UIAutomationClient.csproj (UIAutomationClient)
//---------------------------------------------------------------------------
//
// <copyright file="ClickablePoint.cs" company="Microsoft">
//    Copyright (C) Microsoft Corporation.  All rights reserved.
// </copyright>
// 
//
// Description: Class to get a point that some can click on
//
// History:  
//  06/03/2003 : preid Created
//
//---------------------------------------------------------------------------
 
using System.Windows.Automation;
using MS.Internal.Automation;
using MS.Win32;
using System;
 
namespace System.Windows.Automation
{
    static internal class ClickablePoint
    {
        //------------------------------------------------------
        //
        //  Public Methods
        //
        //------------------------------------------------------
 
        #region Public Methods
 
        // We will not always find the clickable point.   There are times when it would take so long 
        // to locate the point that it is just not worth it, so make a reason effort than quit.
        public static bool  HitTestForClickablePoint(AutomationElement el, out Point pt)
        {
            Rect rect = el.Current.BoundingRectangle;
            
            pt = new Point(0, 0);
            if (rect.Left >= rect.Right || rect.Top >= rect.Bottom)
                return false;    
            
            // if this is not on any monitor than there is no point in going on.  If the element is 
            // off the screen hit testing actually works and would end up returning a point offscreen.
            NativeMethods.RECT winRect = new NativeMethods.RECT((int)rect.Left, (int)rect.Top, (int)rect.Height, (int)rect.Bottom);
            if (SafeNativeMethods.MonitorFromRect( ref winRect, SafeNativeMethods.MONITOR_DEFAULTTONULL ) == IntPtr.Zero)
                return false;
 
 
            // try the center point first
            pt = new Point((rect.Left + rect.Right) / 2, (rect.Top + rect.Bottom) / 2);
 
            AutomationElement hitElement;
            if ( TryPoint( ref pt, el, out hitElement ) )
                return true;
            
            if ( IsTopLevelWindowObscuring( el, rect, hitElement ) )
                return false;
            
            // before we start hit testing more there are some control types that we know where the 
            // clickable point is or we know does not have a clickable point so take care of those here.
            if ( el.Current.ControlType == ControlType.ScrollBar )
                return false;
 
            // Try  the mid point of all four sides
            pt = new Point(rect.Left + (rect.Width /2),  rect.Top + 1);
            if ( TryPoint( ref pt, el ) )
                return true;
            
            pt = new Point(rect.Left + (rect.Width /2),  rect.Bottom - 1);
            if ( TryPoint( ref pt, el ) )
                return true;
            
            pt = new Point( rect.Left + 1, rect.Top + (rect.Height /2) );
            if ( TryPoint( ref pt, el) )
                return true;
            
            pt = new Point(  rect.Right - 1, rect.Top + (rect.Height /2) );
            if ( TryPoint( ref pt, el ) )
                return true;
            
            
            if ( TrySparsePattern( out pt, ref rect, el ) )
                return true;
            
            if ( TryLinePattern( out pt, ref rect, el ) )
                return true;
            
            return false;
        }
        
        #endregion Public Methods
 
        //------------------------------------------------------
        //
        //  Private Methods
        //
        //------------------------------------------------------
 
        #region Private Methods
        
        // Go from the top left on a diagonal the lower right but don't do more than 25 hits
        private static bool TryLinePattern( out Point pt, ref Rect rect,  AutomationElement el)
        {
            double x = rect.Left + 1;
            double y = rect.Top + 1;
            int hits;
 
            // Adjust the number of hits we do based on how big something is
            Double size = rect.Width * rect.Height;
            if ( size < 2500 )
                hits = 10;
            else if ( size < 20000 )
                hits = 18;
            else
                hits = 25;
            
            double xIncr = rect.Width / hits;
            double yIncr = rect.Height / hits;
            for ( int i = 0; i < hits; i++)
            {
                pt = new Point(x,  y);
                if ( TryPoint( ref pt, el ) )
                    return true;
 
                x += xIncr;
                y += yIncr;
            }
 
            pt = new Point(0,  0);
            return false;
        }
 
        // Hit test in a fairly uniform pattern like a grid adjusting the spacing based on the
        // size of element.  Don't more than about 25 hits.
        private static bool TrySparsePattern( out Point pt, ref Rect rect,  AutomationElement el)
        {
            int hits;
            
            // Adjust the number of hits we do based on how big somting is
            Double size = rect.Width * rect.Height;
            if ( size < 2500 )
                hits = 3;
            else if ( size < 20000 )
                hits = 4;
            else
                hits = 5;
 
            // make the scatter pattern fit the proportions of the rect
            double xHits = hits * (rect.Width / rect.Height);
            double yHits = hits * (rect.Height / rect.Width);
            
            double xMovePixels = rect.Width / xHits;
            double yMovePixels = rect.Height / yHits;
            
            return TryPattern( xMovePixels, yMovePixels, out pt, ref rect, el );
        }
        
        // this goes across the rect in icrements of x pixels and the down y pixels and across again...
        private static bool TryPattern(double x, double y, out Point pt, ref Rect rect, AutomationElement el )
        {
            for ( double down = rect.Top + y; down < rect.Bottom; down += y  )
            {
                for ( double across = rect.Left + x; across < rect.Right; across += x )
                {
                    pt = new Point(across,  down);
                    if ( TryPoint(ref pt, el) )
                        return true;
                }
            }
            
            pt = new Point(0,  0);
            return false;
        }
        
        private static bool TryPoint( ref Point pt, AutomationElement el )
        {
            AutomationElement hitEl;
            return TryPoint( ref pt, el, out hitEl );
        }
        
        private static bool TryPoint( ref Point pt, AutomationElement el, out AutomationElement hitEl )
        {
            // If the element is obscured by another window or hidden somehow when we try to hit test we don't get back
            //  the same element.  We want to make sure if someone clicks they click on what they expected to click on.
            hitEl = AutomationElement.FromPoint(pt);
 
            return hitEl == el;
        }
 
        // figure out if there is a top level window totally obscuring the element.  If that is the case 
        // there is not point in going on.  This code assumes that toplevel windows are rects and that
        // everything in that rect covers up what is underneath.
        private static bool IsTopLevelWindowObscuring( AutomationElement target, Rect targetRect, AutomationElement hitTarget)
        {
            // get the toplevel window for the element that we hit on our first try and the element the we are
            // trying to find a clickable point for.   If they are part of the same top level hwnd than there is 
            // no toplevel window obscuring this element.  
            // There is a rather strange case that can occur with apps like media player where the 
            // hitTarget could be something underneth the element.  In this case zorder might be something to 
            // check but this is hwnd specific it may be better for the provider to provide a clickable point.
            AutomationElement hitTargetAncestor = GetTopLevelAncestor(hitTarget);
            if ( GetTopLevelAncestor(target) == hitTargetAncestor || hitTargetAncestor == null )
                return false;
 
            // If this toplevel widow completely covers the element than we are obscured.
            Rect hitTargetAncestorRect = hitTargetAncestor.Current.BoundingRectangle;
            if (hitTargetAncestorRect.Contains( targetRect ) )
                return true;
            
            return false;
        }
 
        private static AutomationElement GetTopLevelAncestor( AutomationElement target )
        {
            AutomationElement root = AutomationElement.RootElement;
            AutomationElement targetAncestor = null;
            
            while (target != root)
            {
                targetAncestor = target;
                
                target = TreeWalker.ControlViewWalker.GetParent( target );
            }
 
            return targetAncestor;
        }
 
    }
    #endregion Private Methods
}