Acc v2

Post your working scripts, libraries and tools.
Descolada
Posts: 1123
Joined: 23 Dec 2021, 02:30

Acc v2

Post by Descolada » 29 Aug 2022, 05:06

Acc library for AHK v2
This is my take on the Acc library. This is not a port of AHK v1 Acc library, but instead a complete redesign to incorporate more object-oriented approaches.
Last updated: 26.01.23


Available at Github and also at the end of this post.


Notable changes (compared to Acc v1)

1) All Acc elements are now array-like objects, where the "Length" property contains the number of children, any nth children can be accessed with element[n], and children can be iterated over with for loops.
2) Acc main functions are contained in the global Acc object
3) Element methods are contained inside element objects
4) Element properties can be used without the "acc" prefix
5) ChildIds have been removed (are handled in the backend), but can be accessed through Element.ChildId
6) Additional methods have been added for elements, such as FindElement, FindElements, Click
7) Acc constants are included in the main Acc object
8) AccViewer is built into the library: when ran directly the AccViewer will show, when included in another script then it won't show (but can be opened by calling Acc.Viewer())

Short introduction

Acc (otherwise known as IAccessible, MSAA, Accessibility library) is a library to get information about (and sometimes interact with) windows, controls, and window elements that are otherwise not accessible with AHKs Control functions.

When Acc.ahk is ran alone, it displays the AccViewer: a window inspector to get Acc information from elements. In the AccViewer on the left side are displayed all the available properties for elements (Value, Name etc), and on the right side the Acc tree which shows how elements are related to eachother. In the bottom is displayed the Acc path (right-click to copy it), which can be used to get that specific element with the Acc library.

To get started with Acc, first include it in your program with #include. If Acc.ahk is in the same folder as your script use #include Acc.ahk, if it is in the Lib folder then use #include <Acc>.

All main Acc properties and methods are accessible through the global variable Acc, which is created when Acc.ahk is included in your script.
To access Acc elements, first you need to get a starting point element (usually a window) and save it in a variable: something like oAcc := Acc.ElementFromHandle(WinTitle). WinTitle uses the same rules as any other AHK function, so SetTitleMatchMode also applies to it.

To get elements from the window element, you can use the Acc path from AccViewer: for example oEl := oAcc[4,1,4] would get the windows 4th sub-element, then the sub-elements 1st child, and then its 4th child.

All the properties displayed in AccViewer can be accessed from the element: oEl.Name will get that elements' name or throw an error if the name doesn't exist. Most properties are read-only, but the Value property can sometimes be changed with oEl.Value := "newvalue"

Element methods can be used in the same way. To do the default action (usually clicking), use oEl.DoDefaultAction(), to highlight the element for 2 seconds use oEl.Highlight(). Dump info about a specific element with oEl.Dump() and dump info about sub-elements as well with oEl.DumpAll() (to get paths and info about all elements in a window, use it on the window element: MsgBox( Acc.ElementFromHandle(WinTitle).DumpAll() ). A more comprehensive list of available methods/properties is down below.

Some examples of how Acc.ahk can be used are included in the Examples folder in Github.

Acc methods

Code: Select all

    ElementFromPoint(x:=unset, y:=unset, activateChromium := True)
        Gets an Acc element from screen coordinates X and Y (NOT relative to the active window).
    ElementFromHandle(hWnd:="", idObject := 0, activateChromium := True)
        Gets an Acc element from a WinTitle, by default the Last Found Window. 
        Additionally idObject can be specified from Acc.ObjId constants (eg to get the Caret location).
    ElementFromChromium(hWnd:="", activateChromium := True)
        Gets an Acc element for the Chromium render control, by default for the Last Found Window.
    GetRootElement()
        Gets the Acc element for the Desktop
    ActivateChromiumAccessibility(hWnd:="") 
        Sends the WM_GETOBJECT message to the Chromium document element and waits for the 
        app to be accessible to Acc. This is called when ElementFromPoint or ElementFromHandle 
        activateChromium flag is set to True. A small performance increase may be gotten 
        if that flag is set to False when it is not needed.
    RegisterWinEvent(callback, eventMin, eventMax?, PID:=0)
        Registers an event or event range from Acc.Event to a callback function and returns
            a new object containing the WinEventHook
        EventMax is an optional variable: if only eventMin and callback are provided, then
            only that single event is registered. If all three arguments are provided, then
            an event range from eventMin to eventMax are registered to the callback function.
        The callback function needs to have two arguments: 
            CallbackFunction(oAcc, EventInfo)

            When the callback function is called:
            oAcc will be the Acc element that called the event
            EventInfo will be an object containing the following properties: 
                Event - an Acc.Event constant
                EventTime - when the event was triggered in system time
                WinID - handle of the window that sent the event 
                ControlID - handle of the control that sent the event, which depending on the
                    window will be the window itself or a control
                ObjId - the object Id (Acc.ObjId) the event was called with
        PID is the Process ID of the process/window the events will be registered from. By default
            events from all windows are registered.
        Unhooking of the event handler will happen once the returned object is destroyed
        (either when overwritten by a constant, or when the script closes).
    ClearHighlights()
        Removes all highlights created by IAccessible.Highlight

    Legacy methods:
    SetWinEventHook(eventMin, eventMax, pCallback)
    UnhookWinEvent(hHook)
    ElementFromPath(ChildPath, hWnd:="A")   => Same as ElementFromHandle[comma-separated path]
    GetRoleText(nRole)                      => Same as element.RoleText
    GetStateText(nState)                    => Same as element.StateText
    Query(pAcc)                             => For internal use

Acc constants

Code: Select all

    Constants can be accessed as properties (eg Acc.ObjId.Caret), or the property name can be
    fetched by getting as an item (eg Acc.ObjId[0xFFFFFFF8])

    ObjId - object identifiers that identify categories of accessible objects within a window. 
    State - used to describe the state of objects in an application UI. These are returned by Element.State or Element.StateText.
    Role - used to describe the roles of various UI objects in an application. These are returned by Element.Role or Element.RoleText.
    NavDir - indicate the spatial (up, down, left, and right) or logical (first child, 
        last, next, and previous) direction used with Element.Navigate() to navigate from one 
        user interface element to another within the same container.
    SelectionFlag - used to specify how an accessible object becomes selected or takes the focus.
        These are used by Element.Select().
    Event - events that are generated by the operating system and by applications. These are
        used when dealing with RegisterWinEvent.
Explanations for the constants are available in Microsoft documentations

Acc element properties

Code: Select all

    Element[n]          => Gets the nth element. Multiple of these can be used like a path:
                                Element[4,1,4] will select 4th childs 1st childs 4th child
                            Path string can also be used with comma-separated numbers or RoleText
                                Element["4,window,4"] will select 4th childs first RoleText=window childs 4th child
                                Element["4,window2,4"] will select the second RoleText=window
                            With a path string "p" followed by a number n will return the nth parent.
                                Element["p2,2"] will get the parent parents second child
                            Conditions (see ValidateCondition) are supported: 
                                Element[4,{Name:"Something"}] will select the fourth childs first child matching the name "Something"
                            Conditions also accept an index (or i) parameter to select from multiple similar elements
                                Element[{Name:"Something", i:3}] selects the third element of elements with name "Something"
                            Negative index will select from the last element
                                Element[{Name:"Something", i:-1}] selects the last element of elements with name "Something"
                            Since index/i needs to be a key-value pair, then to use it with an "or" condition
                            it must be inside an object ("and" condition), for example with key "or":
                                Element[{or:[{Name:"Something"},{Name:"Something else"}], i:2}]
    Name                => Gets or sets the name. All objects support getting this property.
    Value               => Gets or sets the value. Not all objects have a value.
    Role                => Gets the Role of the specified object in integer form. All objects support this property.
    RoleText            => Role converted into text form. All objects support this property.
    Help                => Retrieves the Help property string of an object. Not all objects support this property.
    KeyboardShortcut    => Retrieves the specified object's shortcut key or access key. Not all objects support this property.
    State               => Retrieves the current state in integer form. All objects support this property.
    StateText           => State converted into text form
    Description         => Retrieves a string that describes the visual appearance of the specified object. Not all objects have a description.
    DefaultAction       => Retrieves a string that indicates the object's default action. Not all objects have a default action.
    Focus               => Returns the focused child element (or itself).
                            If no child is focused, an error is thrown
    Selection           => Retrieves the selected children of this object. All objects that support selection must support this property.
    Parent              => Returns the parent element. All objects support this property.
    IsChild             => Checks whether the current element is of child type
    Length              => Returns the number of children the element has
    Location            => Returns the object's current screen location in an object {x,y,w,h}
    Children            => Returns all children as an array (usually not required)
    Exists              => Checks whether the element is still alive and accessible
    ControlID           => ID (hwnd) of the control associated with the element
    WinID               => ID (hwnd) of the window the element belongs to
    accessible          => ComObject of the underlying IAccessible (for internal use)
    childId             => childId of the underlying IAccessible (for internal use)

Acc element methods

Code: Select all

    Select(flags)
        Modifies the selection or moves the keyboard focus of the specified object. 
        flags can be any of the Acc.SelectionFlag constants
    DoDefaultAction()
        Performs the specified object's default action. Not all objects have a default action.
    GetNthChild(n)
        This is equal to element[n]
    GetPath(oTarget)
        Returns the path from the current element to oTarget element.
        The returned path is a comma-separated list of integers corresponding to the order the 
        Acc tree needs to be traversed to access oTarget element from this element.
        If no path is found then an empty string is returned.
    GetLocation(relativeTo:="")
        Returns an object containing the x, y coordinates and width and height: {x:x coordinate, y:y coordinate, w:width, h:height}. 
        relativeTo can be client, window or screen, default is A_CoordModeMouse.
    IsEqual(oCompare)
        Checks whether the element is equal to another element (oCompare)
    FindElement(condition, scope:=4, index:=1, order:=0, depth:=-1) 
        Finds the first element matching the condition (see description under ValidateCondition).
        The returned element also has the "Path" property with the found elements path.

        Condition: A condition object (see ValidateCondition). This condition object can also contain named argument values:
            FindElement({Name:"Something", scope:"Subtree"})
        Scope: the search scope (Acc.TreeScope value): Element, Children, Family (Element+Children), Descendants, SubTree (Element+Descendants). Default is Descendants.
        Index: can be used to search for i-th element. 
            Like the other parameters, this can also be supplied in the condition with index or i:
                FindElement({Name:"Something", i:3}) finds the third element with name "Something"
            Negative index reverses the search direction:
                FindElement({Name:"Something", i:-1}) finds the last element with name "Something"
            Since index/i needs to be a key-value pair, then to use it with an "or" condition
            it must be inside an object ("and" condition), for example with key "or":
                FindElement({or:[{Name:"Something"}, {Name:"Something else"}], index:2})
        Order: defines the order of tree traversal (Acc.TreeTraversalOptions value): 
            Default, LastToFirst, PostOrder. Default is FirstToLast and PreOrder.
    FindElements(condition:=True, scope:=4, depth:=-1)
        Returns an array of elements matching the condition (see description under ValidateCondition)
        The returned elements also have the "Path" property with the found elements path
        By default matches any condition.
    WaitElement(conditionOrPath, timeOut:=-1, scope:=4, index:=1, order:=0, depth:=-1)
        Waits an element to be detectable in the Acc tree. This doesn't mean that the element
        is visible or interactable, use WaitElementExist for that. 
        Timeout less than 1 waits indefinitely, otherwise is the wait time in milliseconds
        A timeout returns 0.
    WaitElementExist(conditionOrPath, timeOut:=-1, scope:=4, index:=1, order:=0, depth:=-1)
        Waits an element exist that matches a condition or a path. 
        Timeout less than 1 waits indefinitely, otherwise is the wait time in milliseconds
        A timeout returns 0.
    WaitNotExist(timeOut:=-1)
        Waits for the element to not exist. 
        Timeout less than 1 waits indefinitely, otherwise is the wait time in milliseconds
        A timeout returns 0.
    Normalize(condition)
        Checks whether the current element or any of its ancestors match the condition, 
        and returns that element. If no element is found, an error is thrown.
    ValidateCondition(condition)
        Checks whether the element matches a provided condition.
        Everything inside {} is an "and" condition, or a singular condition with options
        Everything inside [] is an "or" condition
        "not" key creates a not condition
        "matchmode" key (short form: "mm") defines the MatchMode: StartsWith, Substring, Exact, RegEx (Acc.MATCHMODE values)
        "casesensitive" key (short form: "cs") defines case sensitivity: True=case sensitive; False=case insensitive
        Any other key (but recommended is "or") can be used to use "or" condition inside "and" condition.
        Additionally, when matching for location then partial matching can be used (eg only width and height)
            and relative mode (client, window, screen) can be specified with "relative" or "r".
        An empty object {} is used as "unset" or "N/A" value.

        For methods which use this condition, it can also contain named arguments:
            oAcc.FindElement({Name:"Something", scope:"Subtree", order:"LastToFirst"})
            is equivalent to FindElement({Name:"Something"}, "Subtree",, "LastToFirst")
            is equivalent to FindElement({Name:"Something"}, Acc.TreeScope.SubTree,, Acc.TreeTraversalOptions.LastToFirst)
            is equivalent to FindElement({Name:"Something"}, 7,, 1)

        {Name:"Something"} => Name must match "Something" (case sensitive)
        {Name:"Something", matchmode:"SubString", casesensitive:False} => Name must contain "Something" anywhere inside the Name, case insensitive. matchmode:"SubString" == matchmode:2 == matchmode:Acc.MatchMode.SubString
        {Name:"Something", RoleText:"something else"} => Name must match "Something" and RoleText must match "something else"
        [{Name:"Something", Role:42}, {Name:"Something2", RoleText:"something else"}] => Name=="Something" and Role==42 OR Name=="Something2" and RoleText=="something else"
        {Name:"Something", not:[{RoleText:"something", mm:"Substring"}, {RoleText:"something else", cs:1}]} => Name must match "something" and RoleText cannot match "something" (with matchmode=Substring == matchmode=2) nor "something else" (casesensitive matching)
        {or:[{Name:"Something"},{Name:"Something else"}], or2:[{Role:20},{Role:42}]}
        {Location:{w:200, h:100, r:"client"}} => Location must match width 200 and height 100 relative to client
    Dump(scope:=1, delimiter:=" ", depth:=-1)
        {Name:{}} => Matches for no defined name (outputted by Dump as N/A)
        Outputs relevant information about the element (Name, Value, Location etc)
        Scope is the search scope (Acc.TreeScope value): Element, Children, Family (Element+Children), Descendants, SubTree (Element+Descendants). Default is Element.
    DumpAll(delimiter:=" ", depth:=-1)
        Outputs relevant information about the element and all descendants of the element. This is equivalent to Dump(7)
    Highlight(showTime:=unset, color:="Red", d:=2)
        Highlights the element for a chosen period of time
        Possible showTime values:
            Unset - highlights for 2 seconds, or removes the highlighting
            0 - Indefinite highlighting. If the element object gets destroyed, so does the highlighting.
            Positive integer (eg 2000) - will highlight and pause for the specified amount of time in ms
            Negative integer - will highlight for the specified amount of time in ms, but script execution will continue
            "clear" - removes the highlight
        color can be any of the Color names or RGB values
        d sets the border width
    Click(WhichButton:="left", ClickCount:=1, DownOrUp:="", Relative:="", NoActivate:=False)
        Click the center of the element.
        If WhichButton is a number, then Sleep will be called with that number. 
            Eg Click(200) will sleep 200ms after clicking
        If ClickCount is a number >=10, then Sleep will be called with that number. To click 10+ times and sleep after, specify "ClickCount SleepTime". Ex: Click("left", 200) will sleep 200ms after clicking. 
            Ex: Click("left", "20 200") will left-click 20 times and then sleep 200ms.
        Relative can be offset values for the click (eg "-5 10" would offset the click from the center, X by -5 and Y by +10).
        NoActivate will cause the window not to be brought to focus before clicking if the clickable point is not visible on the screen.
    ControlClick(WhichButton:="left", ClickCount:=1, Options:="")
        ControlClicks the element after getting relative coordinates with GetLocation("client"). 
        If WhichButton is a number, then a Sleep will be called afterwards. Ex: ControlClick(200) will sleep 200ms after clicking. Same for ControlClick("ahk_id 12345", 200)
    Navigate(navDir)
        Navigates in one of the directions specified by Acc.NavDir constants. Not all elements implement this method. 
    HitTest(x, y)
        Retrieves the child element or child object that is displayed at a specific point on the screen.
        This shouldn't be used, since Acc.ElementFromPoint uses this internally

The library itself (periodically updated)

Code: Select all

/*
    Accessibility library for AHK v2

    Authors: Sean, jethrow, Sancarn (v1 code), Descolada
    Special thanks to Lexikos for many tips about v2

    Short introduction:
        Acc v2 in not a port of v1, but instead a complete redesign to incorporate more object-oriented approaches. 

        Notable changes:
        1) All Acc elements are now array-like objects, where the "Length" property contains the number of children, any nth children can be accessed with element[n], and children can be iterated over with for loops.
        2) Acc main functions are contained in the global Acc object
        3) Element methods are contained inside element objects
        4) Element properties can be used without the "acc" prefix
        5) ChildIds have been removed (are handled in the backend), but can be accessed through 
           Element.ChildId
        6) Additional methods have been added for elements, such as FindElement, FindElements, Click
        7) Acc constants are included in the main Acc object
        8) AccViewer is built into the library: when ran directly the AccViewer will show, when included
           in another script then it won't show (but can be opened by calling Acc.Viewer())

    Acc constants/properties:
        Constants can be accessed as properties (eg Acc.ObjId.Caret), or the property name can be
        fetched by getting as an item (eg Acc.ObjId[0xFFFFFFF8])

        ObjId - object identifiers that identify categories of accessible objects within a window. 
        State - used to describe the state of objects in an application UI. These are returned by Element.State or Element.StateText.
        Role - used to describe the roles of various UI objects in an application. These are returned by Element.Role or Element.RoleText.
        NavDir - indicate the spatial (up, down, left, and right) or logical (first child, 
            last, next, and previous) direction used with Element.Navigate() to navigate from one 
            user interface element to another within the same container.
        SelectionFlag - used to specify how an accessible object becomes selected or takes the focus.
            These are used by Element.Select().
        Event - events that are generated by the operating system and by applications. These are
            used when dealing with RegisterWinEvent.

        More thorough explanations for the constants are available in Microsoft documentation:
        https://docs.microsoft.com/en-us/windows/win32/winauto/constants-and-enumerated-types
    
    Acc methods:
        ElementFromPoint(x:=unset, y:=unset, activateChromium := True)
            Gets an Acc element from screen coordinates X and Y (NOT relative to the active window).
        ElementFromHandle(hWnd:="", idObject := "Window", activateChromium := True)
            Gets an Acc element from a WinTitle, by default the Last Found Window. 
            Additionally idObject can be specified from Acc.ObjId constants (eg to get the Caret location).
        ElementFromChromium(hWnd:="", activateChromium := True)
            Gets an Acc element for the Chromium render control, by default for the Last Found Window.
        GetRootElement()
            Gets the Acc element for the Desktop
        ActivateChromiumAccessibility(hWnd:="") 
            Sends the WM_GETOBJECT message to the Chromium document element and waits for the 
            app to be accessible to Acc. This is called when ElementFromPoint or ElementFromHandle 
            activateChromium flag is set to True. A small performance increase may be gotten 
            if that flag is set to False when it is not needed.
        RegisterWinEvent(event, callback, PID:=0) 
        RegisterWinEvent(eventMin, eventMax, callback, PID:=0)
            Registers an event or event range from Acc.Event to a callback function and returns
                a new object containing the WinEventHook
            EventMax is an optional variable: if only eventMin and callback are provided, then
                only that single event is registered. If all three arguments are provided, then
                an event range from eventMin to eventMax are registered to the callback function.
            The callback function needs to have two arguments: 
                CallbackFunction(oAcc, EventInfo)

                When the callback function is called:
                oAcc will be the Acc element that called the event
                EventInfo will be an object containing the following properties: 
                    Event - an Acc.Event constant
                    EventTime - when the event was triggered in system time
                    WinID - handle of the window that sent the event 
                    ControlID - handle of the control that sent the event, which depending on the
                        window will be the window itself or a control
                    ObjId - the object Id (Acc.ObjId) the event was called with
            PID is the Process ID of the process/window the events will be registered from. By default
                events from all windows are registered.
            Unhooking of the event handler will happen once the returned object is destroyed
            (either when overwritten by a constant, or when the script closes).
        ClearHighlights()
            Removes all highlights created by IAccessible.Highlight

        Legacy methods:
        SetWinEventHook(eventMin, eventMax, pCallback)
        UnhookWinEvent(hHook)
        ElementFromPath(ChildPath, hWnd:="A")   => Same as ElementFromHandle[comma-separated path]
        GetRoleText(nRole)                      => Same as element.RoleText
        GetStateText(nState)                    => Same as element.StateText
        Query(pAcc)                             => For internal use

    IAccessible element properties:
        Element[n]          => Gets the nth element. Multiple of these can be used like a path:
                                    Element[4,1,4] will select 4th childs 1st childs 4th child
                               Path string can also be used with comma-separated numbers or RoleText
                                    Element["4,window,4"] will select 4th childs first RoleText=window childs 4th child
                                    Element["4,window2,4"] will select the second RoleText=window
                               With a path string "p" followed by a number n will return the nth parent.
                                    Element["p2,2"] will get the parent parents second child
                               Conditions (see ValidateCondition) are supported: 
                                    Element[4,{Name:"Something"}] will select the fourth childs first child matching the name "Something"
                               Conditions also accept an index (or i) parameter to select from multiple similar elements
                                    Element[{Name:"Something", i:3}] selects the third element of elements with name "Something"
                               Negative index will select from the last element
                                    Element[{Name:"Something", i:-1}] selects the last element of elements with name "Something"
                               Since index/i needs to be a key-value pair, then to use it with an "or" condition
                               it must be inside an object ("and" condition), for example with key "or":
                                    Element[{or:[{Name:"Something"},{Name:"Something else"}], i:2}]
        Name                => Gets or sets the name. All objects support getting this property.
        Value               => Gets or sets the value. Not all objects have a value.
        Role                => Gets the Role of the specified object in integer form. All objects support this property.
        RoleText            => Role converted into text form. All objects support this property.
        Help                => Retrieves the Help property string of an object. Not all objects support this property.
        KeyboardShortcut    => Retrieves the specified object's shortcut key or access key. Not all objects support this property.
        State               => Retrieves the current state in integer form. All objects support this property.
        StateText           => State converted into text form
        Description         => Retrieves a string that describes the visual appearance of the specified object. Not all objects have a description.
        DefaultAction       => Retrieves a string that indicates the object's default action. Not all objects have a default action.
        Focus               => Returns the focused child element (or itself).
                               If no child is focused, an error is thrown
        Selection           => Retrieves the selected children of this object. All objects that support selection must support this property.
        Parent              => Returns the parent element. All objects support this property.
        IsChild             => Checks whether the current element is of child type
        Length              => Returns the number of children the element has
        Location            => Returns the object's current screen location in an object {x,y,w,h}
        Children            => Returns all children as an array (usually not required)
        Exists              => Checks whether the element is still alive and accessible
        ControlID           => ID (hwnd) of the control associated with the element
        WinID               => ID (hwnd) of the window the element belongs to
        accessible          => ComObject of the underlying IAccessible (for internal use)
        childId             => childId of the underlying IAccessible (for internal use)
    
    IAccessible element methods:
        Select(flags)
            Modifies the selection or moves the keyboard focus of the specified object. 
            flags can be any of the Acc.SelectionFlag constants
        DoDefaultAction()
            Performs the specified object's default action. Not all objects have a default action.
        GetNthChild(n)
            This is equal to element[n]
        GetPath(oTarget)
            Returns the path from the current element to oTarget element.
            The returned path is a comma-separated list of integers corresponding to the order the 
            Acc tree needs to be traversed to access oTarget element from this element.
            If no path is found then an empty string is returned.
        GetLocation(relativeTo:="")
            Returns an object containing the x, y coordinates and width and height: {x:x coordinate, y:y coordinate, w:width, h:height}. 
            relativeTo can be client, window or screen, default is A_CoordModeMouse.
        IsEqual(oCompare)
            Checks whether the element is equal to another element (oCompare)
        FindElement(condition, scope:=4, index:=1, order:=0, depth:=-1) 
            Finds the first element matching the condition (see description under ValidateCondition).
            The returned element also has the "Path" property with the found elements path.

            Condition: A condition object (see ValidateCondition). This condition object can also contain named argument values:
                FindElement({Name:"Something", scope:"Subtree"})
            Scope: the search scope (Acc.TreeScope value): Element, Children, Family (Element+Children), Descendants, SubTree (Element+Descendants). Default is Descendants.
            Index: can be used to search for i-th element. 
                Like the other parameters, this can also be supplied in the condition with index or i:
                    FindElement({Name:"Something", i:3}) finds the third element with name "Something"
                Negative index reverses the search direction:
                    FindElement({Name:"Something", i:-1}) finds the last element with name "Something"
                Since index/i needs to be a key-value pair, then to use it with an "or" condition
                it must be inside an object ("and" condition), for example with key "or":
                    FindElement({or:[{Name:"Something"}, {Name:"Something else"}], index:2})
            Order: defines the order of tree traversal (Acc.TreeTraversalOptions value): 
                Default, PostOrder, LastToFirst. Default is FirstToLast and PreOrder.
        FindElements(condition:=True, scope:=4, depth:=-1)
            Returns an array of elements matching the condition (see description under ValidateCondition)
            The returned elements also have the "Path" property with the found elements path
            By default matches any condition.
        WaitElement(conditionOrPath, timeOut:=-1, scope:=4, index:=1, order:=0, depth:=-1)
            Waits an element to be detectable in the Acc tree. This doesn't mean that the element
            is visible or interactable, use WaitElementExist for that. 
            Timeout less than 1 waits indefinitely, otherwise is the wait time in milliseconds
            A timeout returns 0.
        WaitElementExist(conditionOrPath, timeOut:=-1, scope:=4, index:=1, order:=0, depth:=-1)
            Waits an element exist that matches a condition or a path. 
            Timeout less than 1 waits indefinitely, otherwise is the wait time in milliseconds
            A timeout returns 0.
        WaitNotExist(timeOut:=-1)
            Waits for the element to not exist. 
            Timeout less than 1 waits indefinitely, otherwise is the wait time in milliseconds
            A timeout returns 0.
        Normalize(condition)
            Checks whether the current element or any of its ancestors match the condition, 
            and returns that element. If no element is found, an error is thrown.
        ValidateCondition(condition)
            Checks whether the element matches a provided condition.
            Everything inside {} is an "and" condition, or a singular condition with options
            Everything inside [] is an "or" condition
            "not" key creates a not condition
            "matchmode" key (short form: "mm") defines the MatchMode: StartsWith, Substring, Exact, RegEx (Acc.MATCHMODE values)
            "casesensitive" key (short form: "cs") defines case sensitivity: True=case sensitive; False=case insensitive
            Any other key (but recommended is "or") can be used to use "or" condition inside "and" condition.
            Additionally, when matching for location then partial matching can be used (eg only width and height)
                and relative mode (client, window, screen) can be specified with "relative" or "r".
            An empty object {} is used as "unset" or "N/A" value.

            For methods which use this condition, it can also contain named arguments:
                oAcc.FindElement({Name:"Something", scope:"Subtree", order:"LastToFirst"})
                is equivalent to FindElement({Name:"Something"}, "Subtree",, "LastToFirst")
                is equivalent to FindElement({Name:"Something"}, Acc.TreeScope.SubTree,, Acc.TreeTraversalOptions.LastToFirst)
                is equivalent to FindElement({Name:"Something"}, 7,, 1)

            {Name:"Something"} => Name must match "Something" (case sensitive)
            {Name:"Something", matchmode:"SubString", casesensitive:False} => Name must contain "Something" anywhere inside the Name, case insensitive. matchmode:"SubString" == matchmode:2 == matchmode:Acc.MatchMode.SubString
            {Name:"Something", RoleText:"something else"} => Name must match "Something" and RoleText must match "something else"
            [{Name:"Something", Role:42}, {Name:"Something2", RoleText:"something else"}] => Name=="Something" and Role==42 OR Name=="Something2" and RoleText=="something else"
            {Name:"Something", not:[{RoleText:"something", mm:"Substring"}, {RoleText:"something else", cs:1}]} => Name must match "something" and RoleText cannot match "something" (with matchmode=Substring == matchmode=2) nor "something else" (casesensitive matching)
            {or:[{Name:"Something"},{Name:"Something else"}], or2:[{Role:20},{Role:42}]}
            {Location:{w:200, h:100, r:"client"}} => Location must match width 200 and height 100 relative to client
        Dump(scope:=1, delimiter:=" ", depth:=-1)
            {Name:{}} => Matches for no defined name (outputted by Dump as N/A)
            Outputs relevant information about the element (Name, Value, Location etc)
            Scope is the search scope (Acc.TreeScope value): Element, Children, Family (Element+Children), Descendants, SubTree (Element+Descendants). Default is Element.
        DumpAll(delimiter:=" ", depth:=-1)
            Outputs relevant information about the element and all descendants of the element. This is equivalent to Dump(7)
        Highlight(showTime:=unset, color:="Red", d:=2)
            Highlights the element for a chosen period of time
            Possible showTime values:
                Unset - highlights for 2 seconds, or removes the highlighting
                0 - Indefinite highlighting. If the element object gets destroyed, so does the highlighting.
                Positive integer (eg 2000) - will highlight and pause for the specified amount of time in ms
                Negative integer - will highlight for the specified amount of time in ms, but script execution will continue
                "clear" - removes the highlight
            color can be any of the Color names or RGB values
            d sets the border width
        Click(WhichButton:="left", ClickCount:=1, DownOrUp:="", Relative:="", NoActivate:=False)
            Click the center of the element.
            If WhichButton is a number, then Sleep will be called with that number. 
                Eg Click(200) will sleep 200ms after clicking
            If ClickCount is a number >=10, then Sleep will be called with that number. To click 10+ times and sleep after, specify "ClickCount SleepTime". Ex: Click("left", 200) will sleep 200ms after clicking. 
                Ex: Click("left", "20 200") will left-click 20 times and then sleep 200ms.
            Relative can be offset values for the click (eg "-5 10" would offset the click from the center, X by -5 and Y by +10).
            NoActivate will cause the window not to be brought to focus before clicking if the clickable point is not visible on the screen.
        ControlClick(WhichButton:="left", ClickCount:=1, Options:="")
            ControlClicks the element after getting relative coordinates with GetLocation("client"). 
            If WhichButton is a number, then a Sleep will be called afterwards. Ex: ControlClick(200) will sleep 200ms after clicking. Same for ControlClick("ahk_id 12345", 200)
        Navigate(navDir)
            Navigates in one of the directions specified by Acc.NavDir constants. Not all elements implement this method. 
        HitTest(x, y)
            Retrieves the child element or child object that is displayed at a specific point on the screen.
            This shouldn't be used, since Acc.ElementFromPoint uses this internally
    

    Comments about design choices:
        1)  In this library accessing non-existant properties will cause an error to be thrown. 
            This means that if AccViewer reports N/A as a value then the property doesn't exist,
            which is different from the property being "" or 0. Because of this, we have another
            way of differentiating/filtering elements, which may or may not be useful.
        2)  Methods that have Hwnd as an argument have the default value of Last Found Window. 
            This is to be consistent with AHK overall. 
        3)  Methods that will return a starting point Acc element usually have the activateChromium
            option set to True. This is because Chromium-based applications are quite common, but 
            interacting with them via Acc require accessibility to be turned on. It makes sense to 
            detect those windows automatically and activate by default (if needed).
        4)  ElementFromHandle: in AHK it may make more sense for idObject to have the default value of 
            -4 (Acc.ObjId.Client), but using that by default will prevent access from the window title bar,
            which might be useful to have. Usually though, ObjId.Client will be enough, and when
            using control Hwnds then it must be used (otherwise the object for the window will be returned).
*/

#DllLoad oleacc
;DllCall("SetThreadDpiAwarenessContext", "ptr", -4, "ptr") ; For multi-monitor setups, otherwise coordinates might be reported wrong.

if (!A_IsCompiled and A_LineFile=A_ScriptFullPath)
    Acc.Viewer()

class Acc {
    static PropertyFromValue(obj, value) {
        for k, v in obj.OwnProps()
            if value == v
                return k
        throw UnsetItemError("Property item `"" value "`" not found!", -1)
    }
    static PropertyValueGetter := {get: (obj, value) => Acc.PropertyFromValue(obj, value)}
    static RegisteredWinEvents := Map()

    ; MatchMode constants used in condition objects
    static MatchMode := {
        StartsWith:1,
        Substring:2,
        Exact:3,
        RegEx:"Regex"
    }

    ; Used wherever the scope variable is needed (eg Dump, FindElement, FindElements)
    static TreeScope := {
        Element:1,
        Children:2,
        Family:3,
        Descendants:4,
        Subtree:7
    }

    Static TreeTraversalOptions := {
        Default:0,
        PostOrder:1,
        LastToFirst:2,
        PostOrderLastToFirst:3
    }

    ;Https://Msdn.Microsoft.Com/En-Us/Library/Windows/Desktop/Dd373606(V=Vs.85).Aspx
    Static ObjId := {
        Window:0x00000000, 
        SysMenu:0xffffffff, 
        TitleBar:0xfffffffe, 
        Menu:0xfffffffd, 
        Client:0xfffffffc, 
        VScroll:0xfffffffb, 
        HScroll:0xfffffffa, 
        SizeGrip:0xfffffff9, 
        Caret:0xfffffff8, 
        Cursor:0xfffffff7, 
        Alert:0xfffffff6, 
        Sound:0xfffffff5, 
        QueryClassNameIdx:0xfffffff4, 
        NativeOM:0xfffffff0
    }.Defineprop("__Item", This.PropertyValueGetter)

    ;Https://Msdn.Microsoft.Com/En-Us/Library/Windows/Desktop/Dd373609(V=Vs.85).Aspx
    Static State := {
        Normal:0, 
        Unavailable:0x1, 
        Selected:0x2, 
        Focused:0x4, 
        Pressed:0x8, 
        Checked:0x10, 
        Mixed:0x20, 
        Indeterminate:0x20, 
        ReadOnly:0x40, 
        HotTracked:0x80, 
        Default:0x100, 
        Expanded:0x200, 
        Collapsed:0x400, 
        Busy:0x800, 
        Floating:0x1000, 
        Marqueed:0x2000, 
        Animated:0x4000, 
        Invisible:0x8000, 
        Offscreen:0x10000, 
        Sizeable:0x20000, 
        Moveable:0x40000, 
        SelfVoicing:0x80000, 
        Focusable:0x100000, 
        Selectable:0x200000, 
        Linked:0x400000, 
        Traversed:0x800000, 
        MultiSelectable:0x1000000, 
        ExtSelectable:0x2000000, 
        Alert_Low:0x4000000, 
        Alert_Medium:0x8000000, 
        Alert_High:0x10000000, 
        Protected:0x20000000, 
        Valid:0x7fffffff
    }.Defineprop("__Item", This.PropertyValueGetter)

    ;Https://Msdn.Microsoft.Com/En-Us/Library/Windows/Desktop/Dd373608(V=Vs.85).Aspx
    Static Role := {
        TitleBar:0x1,
        MenuBar:0x2, 
        ScrollBar:0x3, 
        Grip:0x4, 
        Sound:0x5, 
        Cursor:0x6, 
        Caret:0x7, 
        Alert:0x8, 
        Window:0x9, 
        Client:0xa, 
        MenuPopup:0xb, 
        MenuItem:0xc, 
        ToolTip:0xd, 
        Application:0xe, 
        Document:0xf, 
        Pane:0x10, 
        Chart:0x11, 
        Dialog:0x12, 
        Border:0x13, 
        Grouping:0x14, 
        Separator:0x15, 
        Toolbar:0x16, 
        StatusBar:0x17, 
        Table:0x18, 
        ColumnHeader:0x19, 
        RowHeader:0x1a, 
        Column:0x1b, 
        Row:0x1c, 
        Cell:0x1d, 
        Link:0x1e, 
        HelpBalloon:0x1f, 
        Character:0x20, 
        List:0x21, 
        ListItem:0x22, 
        Outline:0x23, 
        OutlineItem:0x24, 
        PageTab:0x25, 
        PropertyPage:0x26, 
        Indicator:0x27, 
        Graphic:0x28, 
        StaticText:0x29, 
        Text:0x2a, 
        PushButton:0x2b, 
        CheckButton:0x2c, 
        RadioButton:0x2d, 
        ComboBox:0x2e, 
        Droplist:0x2f, 
        Progressbar:0x30, 
        Dial:0x31, 
        HotkeyField:0x32, 
        Slider:0x33, 
        SpinButton:0x34, 
        Diagram:0x35, 
        Animation:0x36, 
        Equation:0x37, 
        ButtonDropdown:0x38, 
        ButtonMenu:0x39, 
        ButtonDropdownGrid:0x3a, 
        Whitespace:0x3b, 
        PageTabList:0x3c, 
        Clock:0x3d, 
        SplitButton:0x3e, 
        IPAddress:0x3f, 
        OutlineButton:0x40
    }.Defineprop("__Item", This.PropertyValueGetter)
   ;Https://, Msdn.Microsoft.Com/En-Us/Library/Windows/Desktop/Dd373600(V=Vs.85).Aspx
    Static NavDir := {
        Min:0x0, 
        Up:0x1, 
        Down:0x2, 
        Left:0x3, 
        Right:0x4, 
        Next:0x5, 
        Previous:0x6, 
        FirstChild:0x7, 
        LastChild:0x8, 
        Max:0x9
    }.Defineprop("__Item", This.PropertyValueGetter)

    ;Https://Msdn.Microsoft.Com/En-Us/Library/Windows/Desktop/Dd373634(V=Vs.85).Aspx
    Static SelectionFlag := {
        None:0x0, 
        TakeFocus:0x1, 
        TakeSelection:0x2, 
        ExtendSelection:0x4, 
        AddSelection:0x8, 
        RemoveSelection:0x10, 
        Valid:0x1f
    }.Defineprop("__Item", This.PropertyValueGetter)

    ;Msaa Events List:
    ;    Https://Msdn.Microsoft.Com/En-Us/Library/Windows/Desktop/Dd318066(V=Vs.85).Aspx
    ;What Are Win Events:
    ;    Https://Msdn.Microsoft.Com/En-Us/Library/Windows/Desktop/Dd373868(V=Vs.85).Aspx
    ;System-Level And Object-Level Events:
    ;    Https://Msdn.Microsoft.Com/En-Us/Library/Windows/Desktop/Dd373657(V=Vs.85).Aspx
    ;Console Accessibility:
    ;    Https://Msdn.Microsoft.Com/En-Us/Library/Ms971319.Aspx
    Static Event := {
        Min:0x00000001, 
        Max:0x7fffffff, 
        System_Sound:0x0001, 
        System_Alert:0x0002, 
        System_Foreground:0x0003, 
        System_MenuStart:0x0004, 
        System_MenuEnd:0x0005, 
        System_MenuPopupStart:0x0006, 
        System_MenuPopupEnd:0x0007, 
        System_CaptureStart:0x0008, 
        System_CaptureEnd:0x0009, 
        System_MoveSizeStart:0x000a, 
        System_MoveSizeEnd:0x000b, 
        System_ContextHelpStart:0x000c, 
        System_ContextHelpEnd:0x000d, 
        System_DragDropStart:0x000e, 
        System_DragDropEnd:0x000f, 
        System_DialogStart:0x0010, 
        System_DialogEnd:0x0011, 
        System_ScrollingStart:0x0012, 
        System_ScrollingEnd:0x0013, 
        System_SwitchStart:0x0014, 
        System_SwitchEnd:0x0015, 
        System_MinimizeStart:0x0016, 
        System_MinimizeEnd:0x0017, 
        Console_Caret:0x4001, 
        Console_Update_Region:0x4002, 
        Console_Update_Simple:0x4003, 
        Console_Update_Scroll:0x4004, 
        Console_Layout:0x4005, 
        Console_Start_Application:0x4006, 
        Console_End_Application:0x4007, 
        Object_Create:0x8000, 
        Object_Destroy:0x8001, 
        Object_Show:0x8002, 
        Object_Hide:0x8003, 
        Object_Reorder:0x8004, 
        Object_Focus:0x8005, 
        Object_Selection:0x8006, 
        Object_SelectionAdd:0x8007, 
        Object_SelectionRemove:0x8008, 
        Object_SelectionWithin:0x8009, 
        Object_StateChange:0x800a, 
        Object_LocationChange:0x800b, 
        Object_NameChange:0x800c, 
        Object_DescriptionChange:0x800d, 
        Object_ValueChange:0x800e, 
        Object_ParentChange:0x800f, 
        Object_HelpChange:0x8010, 
        Object_DefactionChange:0x8011, 
        Object_AcceleratorChange:0x8012
    }.Defineprop("__Item", This.PropertyValueGetter)
    
    Static WinEvent := {
        OutOfContext:0, 
        SkipOwnThread:1, 
        SkipOwnProcess:2, 
        InContext:3
    }.Defineprop("__Item", This.PropertyValueGetter)
    
    static __HighlightGuis := Map()

    class IAccessible {
        /**
         * Internal method. Creates an Acc element from a raw IAccessible COM object and/or childId,
         * and additionally stores the hWnd of the window the object belongs to.
         * @param accessible IAccessible COM object
         * @param childId IAccessible childId
         * @param wId hWnd of the parent window
         */
        __New(accessible, childId:=0, wId:=0) {
            if ComObjType(accessible, "Name") != "IAccessible"
                throw Error("Could not access an IAccessible Object")
            this.DefineProp("ptr", {value:ComObjValue(accessible)})
            this.DefineProp("accessible", {value:accessible})
            this.DefineProp("childId", {value:childId})
            if wId=0
                try wId := this.WinID
            this.DefineProp("wId", {value:wId})
            this.DefineProp("ObjPtr", {value:ObjPtr(this)})
        }
        __Delete() {
            if Acc.__HighlightGuis.Has(this.ObjPtr) {
                for _, r in Acc.__HighlightGuis[this.ObjPtr]
                    r.Destroy()
                Acc.__HighlightGuis[this.ObjPtr] := []
            }
        }
        /**
         * Internal method. Is a wrapper to access acc properties or methods that take only the 
         * childId as an input value. This usually shouldn't be called, unless the user is
         * trying to access a property/method that is undefined in this library.
         */
        __Get(Name, Params) {
            if !(SubStr(Name,3)="acc") {
                try return this.accessible.acc%Name%[this.childId]
                try return this.accessible.acc%Name%(this.childId) ; try method with self
                try return this.accessible.acc%Name% ; try property
            }
            try return this.accessible.%Name%[this.childId]
            try return this.accessible.%Name%(this.childId)
            return this.accessible.%Name%
        }
        /**
         * Enables array-like use of Acc elements to access child elements. 
         * If value is an integer then the nth corresponding child will be returned. 
         * If value is a string, then it will be parsed as a comma-separated path which
         * allows both indexes (nth child) and RoleText values.
         * If value is an object, then it will be used in a FindElement call with scope set to Children.
         * @returns {Acc.IAccessible}
         */
        __Item[params*] {
            get {
                oAcc := this
                for _, param in params {
                    if IsInteger(param)
                        oAcc := oAcc.GetNthChild(param)
                    else if IsObject(param)
                        oAcc := oAcc.FindElement(param, 2)
                    else if Type(param) = "String"
                        oAcc := Acc.ElementFromPath(param, oAcc, False)
                    else
                        TypeError("Invalid item type!", -1)
                }
                return oAcc
            }
        }
        /**
         * Enables enumeration of Acc elements, usually in a for loop. 
         * Usage:
         * for [index, ] oChild in oElement
         */
        __Enum(varCount) {
            maxLen := this.Length, i := 0, children := this.Children
            EnumElements(&element) {
                if ++i > maxLen
                    return false
                element := children[i]
                return true
            }
            EnumIndexAndElements(&index, &element) {
                if ++i > maxLen
                    return false
                index := i
                element := children[i]
                return true
            }
            return (varCount = 1) ? EnumElements : EnumIndexAndElements
        }
        /**
         * Internal method. Enables setting IAccessible acc properties.
         */
        __Set(Name, Params, Value) {
            if !(SubStr(Name,3)="acc")
                try return this.accessible.acc%Name%[Params*] := Value
            return this.accessible.%Name%[Params*] := Value
        }
        /**
         * Internal method. Enables setting IAccessible acc properties.
         */
        __Call(Name, Params) {
            if !(SubStr(Name,3)="acc")
                try return this.accessible.acc%Name%(Params.Length?Params[1]:0)
            return this.accessible.%Name%(Params*)
        }

        ; Wrappers for native IAccessible methods and properties.

        /**
         * Modifies the selection or moves the keyboard focus of the specified object. 
         * Objects that support selection or receive the keyboard focus should support this method.
         * @param flags One of the SelectionFlag constants
         */
        Select(flags) => (this.accessible.accSelect(IsInteger(flags) ? flags : Acc.SelectionFlag.%flags%,this.childId)) 
        /**
         * Performs the specified object's default action. Not all objects have a default action.
         */
        DoDefaultAction() => (this.accessible.accDoDefaultAction(this.childId))
        /**
         * Retrieves the child element or child object that is displayed at a specific point on the screen.
         * This method usually shouldn't be called. To get the accessible object that is displayed at a point, 
         * use the ElementFromPoint method, which calls this method internally on native IAccessible side.
         */
        HitTest(x, y) => (this.IAccessibleFromVariant(this.accessible.accHitTest(x, y)))
        /**
         * Traverses to another UI element within a container and retrieves the object. 
         * This method is deprecated and should not be used.
         * @param navDir One of the NavDir constants.
         * @returns {Acc.IAccessible}
         */
        Navigate(navDir) {
            navDir := IsInteger(navDir) ? navDir : Acc.NavDir.%navDir%
            varEndUpAt := this.accessible.accNavigate(navDir,this.childId)
            if Type(varEndUpAt) = "ComObject"
                return Acc.IAccessible(Acc.Query(varEndUpAt))
            else if IsInteger(varEndUpAt)
                return Acc.IAccessible(this.accessible, varEndUpAt, this.wId)
            else
                return
        }
        Name {
            get => (this.accessible.accName[this.childId])
            set => (this.accessible.accName[this.childId] := Value)
        } 
        Value {
            get => (this.accessible.accValue[this.childId])
            set => (this.accessible.accValue[this.childId] := Value)
        } 
        Role => (this.accessible.accRole[this.childId]) ; Returns an integer
        RoleText => (Acc.GetRoleText(this.Role)) ; Returns a string
        Help => (this.accessible.accHelp[this.childId])
        KeyboardShortcut => (this.accessible.accKeyboardShortcut[this.childId])
        State => (this.accessible.accState[this.childId]) ; Returns an integer
        StateText => (Acc.GetStateText(this.accessible.accState[this.childId])) ; Returns a string
        Description => (this.accessible.accDescription[this.childId]) ; Returns a string
        DefaultAction => (this.accessible.accDefaultAction[this.childId]) ; Returns a string
        ; Retrieves the Acc element child that has the keyboard focus.
        Focus => (this.IAccessibleFromVariant(this.accessible.accFocus())) 
        ; Returns an array of Acc elements that are the selected children of this object.
        Selection => (this.IAccessibleFromVariant(this.accessible.accSelection()))
        ; Returns the parent of this object as an Acc element
        Parent => (this.IsChild ? Acc.IAccessible(this.accessible,,this.wId) : Acc.IAccessible(Acc.Query(this.accessible.accParent)))

        ; Returns the Hwnd for the control corresponding to this object
        ControlID {
            get {
                if DllCall("oleacc\WindowFromAccessibleObject", "Ptr", this, "uint*", &hWnd:=0) = 0
                    return hWnd
                throw Error("WindowFromAccessibleObject failed", -1)
            }
        }
        ; Returns the Hwnd for the window corresponding to this object
        WinID {
            get {
                if DllCall("oleacc\WindowFromAccessibleObject", "Ptr", this, "uint*", &hWnd:=0) = 0
                    return DllCall("GetAncestor", "UInt", hWnd, "UInt", GA_ROOT := 2)
                throw Error("WindowFromAccessibleObject failed", -1)
            }
        }
        ; Checks whether internally this corresponds to a native IAccessible object or a childId
        IsChild => (this.childId == 0 ? False : True)
        ; Checks whether this object is selected. This is very slow, a better alternative is Element.Selection
        IsSelected { 
            get {
                try oSel := this.Parent.Selection
                return IsSet(oSel) && this.IsEqual(oSel)
            }
        }
        ; Returns the child count of this object
        Length => (this.childId == 0 ? this.accessible.accChildCount : 0)
        ; Checks whether this object still exists and is visible/accessible
        Exists {
            get {
                try {
                    if ((state := this.State) == 32768) || (state == 1) || (((pos := this.Location).x==0) && (pos.y==0) && (pos.w==0) && (pos.h==0))
                        return 0
                } catch
                    return 0
                return 1
            }
        }
        /**
         * Returns an object containing the location of this element
         * @returns {Object} {x: screen x-coordinate, y: screen y-coordinate, w: width, h: height}
         */
        Location {
            get {
                x:=Buffer(4, 0), y:=Buffer(4, 0), w:=Buffer(4, 0), h:=Buffer(4, 0)
                this.accessible.accLocation(ComValue(0x4003, x.ptr, 1), ComValue(0x4003, y.ptr, 1), ComValue(0x4003, w.ptr, 1), ComValue(0x4003, h.ptr, 1), this.childId)
                Return {x:NumGet(x,0,"int"), y:NumGet(y,0,"int"), w:NumGet(w,0,"int"), h:NumGet(h,0,"int")}
            }
        }
        ; Returns all children of this object as an array of Acc elements
        Children {
            get {
                if this.IsChild || !(cChildren := this.accessible.accChildCount)
                    return []
                Children := Array(), varChildren := Buffer(cChildren * (8+2*A_PtrSize))
                try {
                    if DllCall("oleacc\AccessibleChildren", "ptr", this, "int",0, "int", cChildren, "ptr", varChildren, "int*", cChildren) > -1 {
                        Loop cChildren {
                            i := (A_Index-1) * (A_PtrSize * 2 + 8) + 8
                            child := NumGet(varChildren, i, "ptr")
                            Children.Push(NumGet(varChildren, i-8, "ptr") = 9 ? Acc.IAccessible(Acc.Query(child),,this.wId) : Acc.IAccessible(this.accessible, child, this.wId))
                            NumGet(varChildren, i-8, "ptr") = 9 ? ObjRelease(child) : ""
                        }
                        Return Children
                    }
                }
                throw Error("AccessibleChildren DllCall Failed", -1)
            }
        }
        /**
         * Internal method. Used to convert a variant returned by native IAccessible to 
         * an Acc element or an array of Acc elements.
         */
        IAccessibleFromVariant(var) {
            if Type(var) = "ComObject"
                return Acc.IAccessible(Acc.Query(var),,this.wId)
            else if Type(var) = "Enumerator" {
                oArr := []
                Loop {
                    if var.Call(&childId)
                        oArr.Push(this.IAccessibleFromVariant(childId))
                    else 
                        return oArr
                }
            } else if IsInteger(var)
                return Acc.IAccessible(this.accessible,var,this.wId)
            else
                return var
        }
        ; Returns the nth child of this element. Equivalent to Element[n]
        GetNthChild(n) {
            if !IsNumber(n)
                throw TypeError("Child must be an integer", -1)
            n := Integer(n)
            cChildren := this.accessible.accChildCount
            if n > cChildren
                throw IndexError("Child index " n " is out of bounds", -1)
            varChildren := Buffer(cChildren * (8+2*A_PtrSize))
            try {
                if DllCall("oleacc\AccessibleChildren", "ptr", this, "int",0, "int",cChildren, "ptr",varChildren, "int*",cChildren) > -1 {
                    if n < 1
                        n := cChildren + n + 1
                    if n < 1 || n > cChildren
                        throw IndexError("Child index " n " is out of bounds", -1)
                    i := (n-1) * (A_PtrSize * 2 + 8) + 8
                    child := NumGet(varChildren, i, "ptr")
                    oChild := NumGet(varChildren, i-8, "ptr") = 9 ? Acc.IAccessible(Acc.Query(child),,this.wId) : Acc.IAccessible(this.accessible, child, this.wId)
                    NumGet(varChildren, i-8, "ptr") = 9 ? ObjRelease(child) : ""
                    Return oChild
                }
            }
            throw Error("AccessibleChildren DllCall Failed", -1)
        }

        /**
         * Returns the path from the current element to oTarget element.
         * The returned path is a comma-separated list of integers corresponding to the order the 
         * IAccessible tree needs to be traversed to access oTarget element from this element.
         * If no path is found then an empty string is returned.
         * @param oTarget An Acc element.
         */
        GetPath(oTarget) {
            if Type(oTarget) != "Acc.IAccessible"
                throw TypeError("oTarget must be a valid Acc element!", -1)
            oNext := oTarget, oPrev := oTarget, path := ""
            try {
                while !this.IsEqual(oNext)
                    for i, oChild in oNext := oNext.Parent {
                        if oChild.IsEqual(oPrev) {
                            path := i "," path, oPrev := oNext
                            break
                        }
                    }
                path := SubStr(path, 1, -1)
                if Acc.ElementFromPath(path, this, False).IsEqual(oTarget)
                    return path
            }
            oFind := this.FindElement({IsEqual:oTarget})
            return oFind ? oFind.Path : ""
        }
        /**
         * Returns an object containing the x, y coordinates and width and height: {x:x coordinate, y:y coordinate, w:width, h:height}. 
         * @param relativeTo Coordinate mode, which can be client, window or screen. Default is A_CoordModeMouse.
         * @returns {Object} {x: relative x-coordinate, y: relative y-coordinate, w: width, h: height}
         */
        GetLocation(relativeTo:="") { 
            relativeTo := (relativeTo == "") ? A_CoordModeMouse : relativeTo, loc := this.Location
            if (relativeTo = "screen")
                return loc
            else if (relativeTo = "window") {
                RECT := Buffer(16)
                DllCall("user32\GetWindowRect", "Int", this.wId, "Ptr", RECT)
                return {x:(loc.x-NumGet(RECT, 0, "Int")), y:(loc.y-NumGet(RECT, 4, "Int")), w:loc.w, h:loc.h}
            } else if (relativeTo = "client") {
                pt := Buffer(8), NumPut("int",loc.x,pt), NumPut("int",loc.y,pt,4)
                DllCall("ScreenToClient", "Int", this.wId, "Ptr", pt)
                return {x:NumGet(pt,0,"int"), y:NumGet(pt,4,"int"), w:loc.w, h:loc.h}
            } else
                throw Error(relativeTo "is not a valid CoordMode",-1)
        }
        /**
         * Checks whether this element is equal to another element
         * @param oCompare The Acc element to be compared against.
         */
        IsEqual(oCompare) {
            loc1 := {x:0,y:0,w:0,h:0}, loc2 := {x:0,y:0,w:0,h:0}
            try loc1 := this.Location
            catch { ; loc1 unset
                loc1 := {x:0,y:0,w:0,h:0}
                try return oCompare.Location && 0 ; if loc2 is set then code will return
            }
            try loc2 := oCompare.Location
            if (loc1.x != loc2.x) || (loc1.y != loc2.y) || (loc1.w != loc2.w) || (loc1.h != loc2.h)
                return 0
            for _, v in ((loc1.x = 0) && (loc1.y = 0) && (loc1.w = 0) && (loc1.h = 0)) ? ["Role", "Value", "Name", "State", "DefaultAction", "Description", "KeyboardShortcut", "Help"] : ["Role", "Name"] {
                try v1 := this.%v%
                catch { ; v1 unset
                    try v2 := oCompare.%v%
                    catch ; both unset, continue
                        continue
                    return 0 ; v1 unset, v2 set 
                }
                try v2 := oCompare.%v%
                catch ; v1 set, v2 unset
                    return 0
                if v1 != v2 ; both set
                    return 0
            }
            return 1
        }
        /**
         * Finds the first element matching a set of conditions.
         * The returned element also has a "Path" property with the found elements path
         * @param condition Condition object (see ValidateCondition)
         * @param scope The search scope (Acc.TreeScope value): Element, Children, Family (Element+Children), Descendants, SubTree (Element+Descendants). Default is Descendants.
         * @param index Looks for the n-th element matching the condition
         * @param order The order of tree traversal (Acc.TreeTraversalOptions value): Default, LastToFirst, PostOrder. Default is FirstToLast and PreOrder.
         * @param depth Maximum level of depth for the search. Default is no limit.
         * @returns {Acc.IAccessible}
         */
        FindElement(condition, scope:=4, index:=1, order:=0, depth:=-1) {
            if IsObject(condition) {
                for key in ["index", "scope", "depth", "order"]
                if condition.HasOwnProp(key)
                    %key% := condition.%key%
                if condition.HasOwnProp("i")
                    index := condition.i
                if index < 0
                    order |= 2, index := -index
                else if index = 0
                    throw Error("Condition index cannot be 0", -1)
            }
            scope := IsInteger(scope) ? scope : Acc.TreeScope.%scope%, order := IsInteger(order) ? order : Acc.TreeTraversalOptions.%order%

            if order&1
                return order&2 ? PostOrderLastToFirstRecursiveFind(this, condition, scope,, ++depth) : PostOrderFirstToLastRecursiveFind(this, condition, scope,, ++depth)
            if scope&1
                if this.ValidateCondition(condition) && (--index = 0)
                    return this.DefineProp("Path", {value:""})
            if scope>1
                return order&2 ? PreOrderLastToFirstRecursiveFind(this, condition, scope,, depth) : PreOrderFirstToLastRecursiveFind(this, condition, scope,,depth)

            PreOrderFirstToLastRecursiveFind(element, condition, scope:=4, path:="", depth:=-1) {
                --depth
                for i, child in element.Children {
                    if child.ValidateCondition(condition) && (--index = 0)
                        return child.DefineProp("Path", {value:path (path?",":"") i})
                    else if (scope&4) && (depth != 0) && (rf := PreOrderFirstToLastRecursiveFind(child, condition,, path (path?",":"") i, depth))
                        return rf 
                }
            }
            PreOrderLastToFirstRecursiveFind(element, condition, scope:=4, path:="", depth:=-1) {
                children := element.Children, length := children.Length + 1, --depth
                Loop (length - 1) {
                    child := children[length-A_index]
                    if child.ValidateCondition(condition) && (--index = 0)
                        return child.DefineProp("Path", {value:path (path?",":"") (length-A_index)})
                    else if scope&4 && (depth != 0) && (rf := PreOrderLastToFirstRecursiveFind(child, condition,, path (path?",":"") (length-A_index), depth))
                        return rf 
                }
            }
            PostOrderFirstToLastRecursiveFind(element, condition, scope:=4, path:="", depth:=-1) {
                if (--depth != 0) && scope>1 {
                    for i, child in element.Children {
                        if (rf := PostOrderFirstToLastRecursiveFind(child, condition, (scope & ~2)|1, path (path?",":"") i, depth))
                            return rf 
                    }
                }
                if scope&1 && element.ValidateCondition(condition) && (--index = 0)
                    return element.DefineProp("Path", {value:path})
            }
            PostOrderLastToFirstRecursiveFind(element, condition, scope:=4, path:="", depth:=-1) {
                if (--depth != 0) && scope>1 {
                    children := element.Children, length := children.Length + 1
                    Loop (length - 1) {
                        if (rf := PostOrderLastToFirstRecursiveFind(children[length-A_index], condition, (scope & ~2)|1, path (path?",":"") (length-A_index), depth))
                            return rf 
                    }
                }
                if scope&1 && element.ValidateCondition(condition) && (--index = 0)
                    return element.DefineProp("Path", {value:path})
            }
        }
        FindFirst(args*) => this.FindElement(args*)
        /**
         * Returns an array of elements matching the condition (see description under ValidateCondition)
         * The returned elements also have the "Path" property with the found elements path
         * @param condition Condition object (see ValidateCondition). Default is to match any condition.
         * @param scope The search scope (Acc.TreeScope value): Element, Children, Family (Element+Children), Descendants, SubTree (Element+Descendants). Default is Descendants.
         * @param depth Maximum level of depth for the search. Default is no limit.
         * @returns {[Acc.IAccessible]}
         */
        FindElements(condition:=True, scope:=4, depth:=-1) {
            if Type(condition) = "Object" {
                if condition.HasOwnProp("scope")
                    scope := condition.scope
                if condition.HasOwnProp("depth")
                    depth := condition.depth
            }

            matches := [], ++depth, scope := IsInteger(scope) ? scope : Acc.TreeScope.%scope%
            if scope&1
                if this.ValidateCondition(condition)
                    matches.Push(this.DefineProp("Path", {value:""}))
            if scope>1
                RecursiveFind(this, condition, (scope|1)^1, &matches,, depth)
            return matches
            RecursiveFind(element, condition, scope, &matches, path:="", depth:=-1) {
                if scope>1 {
                    --depth
                    for i, child in element {
                        if child.ValidateCondition(condition)
                            matches.Push(child.DefineProp("Path", {value:path (path?",":"") i}))
                        if scope&4 && (depth != 0)
                            RecursiveFind(child, condition, scope, &matches, path (path?",":"") i, depth)
                    }
                }
            }          
        }
        FindAll(args*) => this.FindElements(args*)
        /**
         * Waits for an element matching a condition or path to exist in the Acc tree.
         * Element being in the Acc tree doesn't mean it's necessarily visible or interactable,
         * use WaitElementExist for that.
         * @param conditionOrPath Condition object (see ValidateCondition), or Acc path as a string (comma-separated numbers)
         * @param timeOut Timeout in milliseconds. Default in indefinite waiting.
         * @param scope The search scope (Acc.TreeScope value): Element, Children, Family (Element+Children), Descendants, SubTree (Element+Descendants). Default is Descendants.
         * @param index Looks for the n-th element matching the condition
         * @param order The order of tree traversal (Acc.TreeTraversalOptions value): Default, PostOrder, LastToFirst. Default is FirstToLast and PreOrder.
         * @param depth Maximum level of depth for the search. Default is no limit.
         * @returns {Acc.IAccessible}
         */
        WaitElement(conditionOrPath, timeOut:=-1, scope:=4, index:=1, order:=0, depth:=-1) {
            if Type(conditionOrPath) = "Object" && conditionOrPath.HasOwnProp("timeOut")
                timeOut := conditionOrPath.timeOut
            waitTime := A_TickCount + timeOut
            while ((timeOut < 1) ? 1 : (A_tickCount < waitTime)) {
                try return IsObject(conditionOrPath) ? this.FindElement(conditionOrPath, scope, index, depth) : this[conditionOrPath]
                Sleep 40
            }
        }
        /**
         * Waits for an element matching a condition or path to appear.
         * @param conditionOrPath Condition object (see ValidateCondition), or Acc path as a string (comma-separated numbers)
         * @param timeOut Timeout in milliseconds. Default in indefinite waiting.
         * @param scope The search scope (Acc.TreeScope value): Element, Children, Family (Element+Children), Descendants, SubTree (Element+Descendants). Default is Descendants.
         * @param index Looks for the n-th element matching the condition
         * @param order The order of tree traversal (Acc.TreeTraversalOptions value): Default, LastToFirst, PostOrder. Default is FirstToLast and PreOrder.
         * @param depth Maximum level of depth for the search. Default is no limit.
         * @returns {Acc.IAccessible}
         */
         WaitElementExist(conditionOrPath, timeOut:=-1, scope:=4, index:=1, order:=0, depth:=-1) {
            if Type(conditionOrPath) = "Object" && conditionOrPath.HasOwnProp("timeOut")
                    timeOut := conditionOrPath.timeOut
            waitTime := A_TickCount + timeOut
            while ((timeOut < 1) ? 1 : (A_tickCount < waitTime)) {
                try {
                    oFind := IsObject(conditionOrPath) ? this.FindElement(conditionOrPath, scope, index, depth) : this[conditionOrPath]
                    if oFind.Exists
                        return oFind
                }
                Sleep 40
            }
        }
        /**
         * Waits for this element to not exist. Returns True if the element disappears before the timeout.
         * @param timeOut Timeout in milliseconds. Default in indefinite waiting.
         */
        WaitNotExist(timeOut:=-1) {
            waitTime := A_TickCount + timeOut
            while ((timeOut < 1) ? 1 : (A_tickCount < waitTime)) {
                if !this.Exists
                    return 1
                Sleep 40
            }
        }
        /**
         * Checks whether the current element or any of its ancestors match the condition, 
         * and returns that Acc element. If no element is found, an error is thrown.
         * @param condition Condition object (see ValidateCondition)
         * @returns {Acc.IAccessible}
         */
        Normalize(condition) {
            if this.ValidateCondition(condition)
                return this
            oEl := this
            Loop {
                try {
                    oEl := oEl.Parent
                    if oEl.ValidateCondition(condition)
                        return oEl
                } catch
                    break
            }
            return 0
        }

        /*
            Checks whether the element matches a provided condition.
            Everything inside {} is an "and" condition
            Everything inside [] is an "or" condition
            Object key "not" creates a not condition

            matchmode key defines the MatchMode: StartsWith, Substring, Exact, RegEx (Acc.MATCHMODE values)

            casesensitive key defines case sensitivity: True=case sensitive; False=case insensitive

            {Name:"Something"} => Name must match "Something" (case sensitive)
            {Name:"Something", RoleText:"something else"} => Name must match "Something" and RoleText must match "something else"
            [{Name:"Something", Role:42}, {Name:"Something2", RoleText:"something else"}] => Name=="Something" and Role==42 OR Name=="Something2" and RoleText=="something else"
            {Name:"Something", not:[RoleText:"something", RoleText:"something else"]} => Name must match "something" and RoleText cannot match "something" nor "something else"
        */
        ValidateCondition(oCond) {
            if !IsObject(oCond)
                return !!oCond ; if oCond is not an object, then it is treated as True or False condition
            if Type(oCond) = "Array" { ; or condition
                for _, c in oCond
                    if this.ValidateCondition(c)
                        return 1
                return 0
            }
            matchmode := 3, casesensitive := 1, notCond := False
            for p in ["matchmode", "mm"]
                if oCond.HasOwnProp(p)
                    matchmode := oCond.%p%
            try matchmode := IsInteger(matchmode) ? matchmode : Acc.MATCHMODE.%matchmode%
            for p in ["casesensitive", "cs"]
                if oCond.HasOwnProp(p)
                    casesensitive := oCond.%p%
            for prop, cond in oCond.OwnProps() {
                switch Type(cond) { ; and condition
                    case "String", "Integer":
                        if prop ~= "i)^(index|i|matchmode|mm|casesensitive|cs|scope|timeout)$"
                            continue
                        propValue := ""
                        try propValue := this.%prop%
                        switch matchmode, 0 {
                            case 2:
                                if !InStr(propValue, cond, casesensitive)
                                    return 0
                            case 1:
                                if !((casesensitive && (SubStr(propValue, 1, StrLen(cond)) == cond)) || (!casesensitive && (SubStr(propValue, 1, StrLen(cond)) = cond)))
                                    return 0
                            case "Regex":
                                if !(propValue ~= cond)
                                    return 0
                            default:
                                if !((casesensitive && (propValue == cond)) || (!casesensitive && (propValue = cond)))
                                    return 0
                        }
                    case "Acc.IAccessible":
                        if (prop="IsEqual") ? !this.IsEqual(cond) : !this.ValidateCondition(cond)
                            return 0
                    default:
                        if (HasProp(cond, "Length") ? cond.Length = 0 : ObjOwnPropCount(cond) = 0) {
                            try return this.%prop% && 0
                            catch
                                return 1
                        } else if (prop = "Location") {
                            try loc := cond.HasOwnProp("relative") ? this.GetLocation(cond.relative) 
                                : cond.HasOwnProp("r") ? this.GetLocation(cond.r) 
                                : this.Location
                            catch
                                return 0
                            for lprop, lval in cond.OwnProps() {
                                if (!((lprop = "relative") || (lprop = "r")) && (loc.%lprop% != lval))
                                    return 0
                            }
                        } else if ((prop = "not") ? this.ValidateCondition(cond) : !this.ValidateCondition(cond))
                            return 0
                }
            }
            return 1
        }
        /**
         * Outputs relevant information about the element
         * @param scope The search scope: Element, Children, Family (Element+Children), Descendants, SubTree (Element+Descendants). Default is Element.
         * @param delimiter The delimiter separating the outputted properties
         * @param depth Maximum number of levels to dump. Default is no limit.
         * @returns {String}
         */
        Dump(scope:=1, delimiter:=" ", depth:=-1) {
            out := "", scope := IsInteger(scope) ? scope : Acc.TreeScope.%scope%
            if scope&1 {
                RoleText := "N/A", Role := "N/A", Value := "N/A", Name := "N/A", StateText := "N/A", State := "N/A", DefaultAction := "N/A", Description := "N/A", KeyboardShortcut := "N/A", Help := "N/A", Location := {x:"N/A",y:"N/A",w:"N/A",h:"N/A"}
                for _, v in ["RoleText", "Role", "Value", "Name", "StateText", "State", "DefaultAction", "Description", "KeyboardShortcut", "Help", "Location"]
                    try %v% := this.%v%
                out := "RoleText: " RoleText delimiter "Role: " Role delimiter "[Location: {x:" Location.x ",y:" Location.y ",w:" Location.w ",h:" Location.h "}]" delimiter "[Name: " Name "]" delimiter "[Value: " Value  "]" (StateText ? delimiter "[StateText: " StateText "]" : "") (State ? delimiter "[State: " State "]" : "") (DefaultAction ? delimiter "[DefaultAction: " DefaultAction "]" : "") (Description ? delimiter "[Description: " Description "]" : "") (KeyboardShortcut ? delimiter "[KeyboardShortcut: " KeyboardShortcut "]" : "") (Help ? delimiter "[Help: " Help "]" : "") (this.childId ? delimiter "ChildId: " this.childId : "") "`n"
            }
            if scope&4
                return Trim(RecurseTree(this, out,, depth), "`n")
            if scope&2 {
                for n, oChild in this.Children
                    out .= n ":" delimiter oChild.Dump() "`n"
            }
            return Trim(out, "`n")

            RecurseTree(oAcc, tree, path:="", depth:=-1) {
                if depth > 0 {
                    StrReplace(path, "," , , , &count)
                    if count >= (depth-1)
                        return tree
                }
                try {
                    if !oAcc.Length
                        return tree
                } catch
                    return tree
                
                For i, oChild in oAcc.Children {
                    tree .= path (path?",":"") i ":" delimiter oChild.Dump() "`n"
                    tree := RecurseTree(oChild, tree, path (path?",":"") i, depth)
                }
                return tree
            }
        }
        /**
         * Outputs relevant information about the element and all its descendants.
         * @param delimiter The delimiter separating the outputted properties
         * @param depth Maximum number of levels to dump. Default is no limit.
         * @returns {String}
         */
        DumpAll(delimiter:=" ", depth:=-1) => this.Dump(5, delimiter, depth)
        ; Same as Dump()
        ToString() => this.Dump()

        /**
         * Highlights the element for a chosen period of time.
         * @param showTime Can be one of the following:
         *     Unset - highlights for 2 seconds, or removes the highlighting
         *     0 - Indefinite highlighting. If the element object gets destroyed, so does the highlighting.
         *     Positive integer (eg 2000) - will highlight and pause for the specified amount of time in ms
         *     Negative integer - will highlight for the specified amount of time in ms, but script execution will continue
         *     "clear" - removes the highlight
         * @param color The color of the highlighting. Default is red.
         * @param d The border thickness of the highlighting in pixels. Default is 2.
         * @returns {Acc.IAccessible}
         */
        Highlight(showTime:=unset, color:="Red", d:=2) {
            if !Acc.__HighlightGuis.Has(this.ObjPtr)
                Acc.__HighlightGuis[this.ObjPtr] := []
            if (!IsSet(showTime) && Acc.__HighlightGuis[this.ObjPtr].Length) || (IsSet(showTime) && showTime = "clear") {
                for _, r in Acc.__HighlightGuis[this.ObjPtr]
                    r.Destroy()
                Acc.__HighlightGuis[this.ObjPtr] := []
                return this
            } else if !IsSet(showTime)
                showTime := 2000
            try loc := this.Location
            if !IsSet(loc) || !IsObject(loc)
                return this
            Loop 4 {
                Acc.__HighlightGuis[this.ObjPtr].Push(Gui("+AlwaysOnTop -Caption +ToolWindow -DPIScale +E0x08000000"))
            }
            Loop 4
            {
                i:=A_Index
                , x1:=(i=2 ? loc.x+loc.w : loc.x-d)
                , y1:=(i=3 ? loc.y+loc.h : loc.y-d)
                , w1:=(i=1 or i=3 ? loc.w+2*d : d)
                , h1:=(i=2 or i=4 ? loc.h+2*d : d)
                Acc.__HighlightGuis[this.ObjPtr][i].BackColor := color
                Acc.__HighlightGuis[this.ObjPtr][i].Show("NA x" . x1 . " y" . y1 . " w" . w1 . " h" . h1)
            }
            if showTime > 0 {
                Sleep(showTime)
                this.Highlight()
            } else if showTime < 0
                SetTimer(ObjBindMethod(this, "Highlight", "clear"), -Abs(showTime))
            return this
        }
        ClearHighlight() => this.Highlight("clear")

        /**
         * Clicks the center of the element.
         * @param WhichButton Left (default), Right, Middle (or just the first letter of each of these); or the fourth or fifth mouse button (X1 or X2).
         *     If WhichButton is an Integer, then Sleep will be called with that number. 
         *     Example: Click(200) will sleep 200ms after clicking
         * @param ClickCount The number of times to click the mouse.
         *     If ClickCount is a number >=10, then Sleep will be called with that number. 
         *     To click 10+ times and sleep after, specify "ClickCount SleepTime". 
         *     Example: Click("left", 200) will sleep 200ms after clicking. 
         *     Example: Click("left", "20 200") will left-click 20 times and then sleep 200ms.
         * @param DownOrUp This component is normally omitted, in which case each click consists of a down-event followed by an up-event. 
         *     Otherwise, specify the word Down (or the letter D) to press the mouse button down without releasing it. 
         *     Later, use the word Up (or the letter U) to release the mouse button.
         * @param Relative Optional offset values for both X and Y (eg "-5 10" would offset X by -5 and Y by +10).
         * @param NoActivate Setting NoActivate to True will prevent the window from being brought to front if the clickable point is not visible on screen.
         * @returns {Acc.IAccessible}
         */
        Click(WhichButton:="left", ClickCount:=1, DownOrUp:="", Relative:="", NoActivate:=False) {		
            rel := [0,0], pos := this.Location, saveCoordMode := A_CoordModeMouse, cCount := 1, SleepTime := -1
            if (Relative && !InStr(Relative, "rel"))
                rel := StrSplit(Relative, " "), Relative := ""
            if IsInteger(WhichButton)
                SleepTime := WhichButton, WhichButton := "left"
            if !IsInteger(ClickCount) && InStr(ClickCount, " ") {
                sCount := StrSplit(ClickCount, " ")
                cCount := sCount[1], SleepTime := sCount[2]
            } else if ClickCount > 9 {
                SleepTime := cCount, cCount := 1
            }
            if (!NoActivate && (Acc.WindowFromPoint(pos.x+pos.w//2+rel[1], pos.y+pos.h//2+rel[2]) != this.wId)) {
                WinActivate(this.wId)
                WinWaitActive(this.wId)
            }
            CoordMode("Mouse", "Screen")
            Click(pos.x+pos.w//2+rel[1] " " pos.y+pos.h//2+rel[2] " " WhichButton (ClickCount ? " " ClickCount : "") (DownOrUp ? " " DownOrUp : "") (Relative ? " " Relative : ""))
            CoordMode("Mouse", saveCoordMode)
            Sleep(SleepTime)
            return this
        }

        /**
         * ControlClicks the center of the element after getting relative coordinates with GetLocation("client").
         * @param WhichButton The button to click: LEFT, RIGHT, MIDDLE (or just the first letter of each of these). 
         *     If omitted or blank, the LEFT button will be used.
         *     If an Integer is provided then a Sleep will be called afterwards. 
         *     Ex: ControlClick(200) will sleep 200ms after clicking.
         * @returns {Acc.IAccessible}
         */
        ControlClick(WhichButton:="left", ClickCount:=1, Options:="") { 
            pos := this.GetLocation("client")
            ControlClick("X" pos.x+pos.w//2 " Y" pos.y+pos.h//2, this.wId,, IsInteger(WhichButton) ? "left" : WhichButton, ClickCount, Options)
            if IsInteger(WhichButton)
                Sleep(WhichButton)
            return this
        }
    }

    /**
     * Returns an Acc element from a screen coordinate. If both coordinates are omitted then
     * the element under the mouse will be returned.
     * @param x The x-coordinate
     * @param y The y-coordinate
     * @param activateChromium Whether to turn on accessibility for Chromium-based windows. Default is True.
     * @returns {Acc.IAccessible}
     */
    static ElementFromPoint(x:=unset, y:=unset, activateChromium:=True) {
        if !(IsSet(x) && IsSet(y))
            DllCall("GetCursorPos", "int64P", &pt64:=0), x := 0xFFFFFFFF & pt64, y := pt64 >> 32
        else
            pt64 := y << 32 | (x & 0xFFFFFFFF)
        wId := DllCall("GetAncestor", "UInt", DllCall("user32.dll\WindowFromPoint", "int64",  pt64), "UInt", 2) ; hwnd from point by SKAN. 2 = GA_ROOT
        if activateChromium
            Acc.ActivateChromiumAccessibility(wId)
        pvarChild := Buffer(8 + 2 * A_PtrSize)
        if DllCall("oleacc\AccessibleObjectFromPoint", "int64",pt64, "ptr*",&ppAcc := 0, "ptr",pvarChild) = 0
        {	; returns a pointer from which we get a Com Object
            return Acc.IAccessible(ComValue(9, ppAcc), NumGet(pvarChild,8,"UInt"), wId)
        }
    }
    ; Wrapper for native function name
    static ObjectFromPoint(args*) => Acc.ElementFromPoint(args*)
    
    /**
     * Returns an Acc element corresponding to the provided window Hwnd. 
     * @param hWnd The window Hwnd. Default is Last Found Window.
     * @param idObject An Acc.ObjId constant. Default is Acc.ObjId.Window (value 0). 
     *     Note that to get objects by control Hwnds, use ObjId.Client (value -4).
     * @param activateChromium Whether to turn on accessibility for Chromium-based windows. Default is True.
     * @returns {Acc.IAccessible}
     */
    static ElementFromHandle(hWnd:="", idObject := "Window", activateChromium:=True) {
        if !IsInteger(idObject)
            try idObject := Acc.ObjId.%idObject%
        if !IsInteger(hWnd)
            hWnd := WinExist(hWnd)
        if !hWnd
            throw Error("Invalid window handle or window not found", -1)
        if activateChromium
            Acc.ActivateChromiumAccessibility(hWnd)
        IID := Buffer(16)
        if DllCall("oleacc\AccessibleObjectFromWindow", "ptr",hWnd, "uint",idObject &= 0xFFFFFFFF
                , "ptr",-16 + NumPut("int64", idObject == 0xFFFFFFF0 ? 0x46000000000000C0 : 0x719B3800AA000C81, NumPut("int64", idObject == 0xFFFFFFF0 ? 0x0000000000020400 : 0x11CF3C3D618736E0, IID))
                , "ptr*", ComObj := ComValue(9,0)) = 0
            Return Acc.IAccessible(ComObj,,hWnd)
    }
    ; Wrapper for native function name
    static ObjectFromWindow(args*) => Acc.ElementFromHandle(args*)
    /**
     * Returns an Acc element corresponding to the provided windows Chrome_RenderWidgetHostHWND control. 
     * @param hWnd The window Hwnd. Default is Last Found Window.
     * @param activateChromium Whether to turn on accessibility. Default is True.
     * @returns {Acc.IAccessible}
     */
    static ElementFromChromium(hWnd:="", activateChromium:=True) {
        if !IsInteger(hWnd)
            hWnd := WinExist(hWnd)
        if !hWnd
            throw Error("Invalid window handle or window not found", -1)
        if activateChromium
            Acc.ActivateChromiumAccessibility(hWnd)
        if !(cHwnd := ControlGetHwnd("Chrome_RenderWidgetHostHWND1", hWnd))
            throw Error("Chromium render element was not found", -1)
        return Acc.ElementFromHandle(cHwnd, -4,False)
    }
    static ObjectFromChromium(args*) => Acc.ElementFromChromium(args*)
    /**
     * Returns an Acc element from a path string (comma-separated integers or RoleText values)
     * @param ChildPath Comma-separated indexes for the tree traversal. 
     *     Instead of an index, RoleText is also permitted.
     * @param hWnd Window handle or IAccessible object. Default is Last Found Window.
     * @param activateChromium Whether to turn on accessibility for Chromium-based windows. Default is True.
     * @returns {Acc.IAccessible}
     */
    static ElementFromPath(ChildPath, hWnd:="", activateChromium:=True) {
        if Type(hWnd) = "Acc.IAccessible"
            oAcc := hWnd
        else {
            if activateChromium
                Acc.ActivateChromiumAccessibility(hWnd)
            oAcc := Acc.ElementFromHandle(hWnd)
        }
        ChildPath := StrReplace(StrReplace(ChildPath, ".", ","), " ")
        Loop Parse ChildPath, ","
        {
            if IsInteger(A_LoopField)
                oAcc := oAcc.GetNthChild(A_LoopField)
            else {
                RegExMatch(A_LoopField, "(\D+)(\d*)", &m), i := m[2] || 1, c := 0
                if m[1] = "p" {
                    Loop i
                        oAcc := oAcc.Parent
                    continue
                }
                for oChild in oAcc {
                    try {
                        if (StrReplace(oChild.RoleText, " ") = m[1]) && (++c = i) {
                            oAcc := oChild
                            break
                        }
                    }
                }
            }
        }
        Return oAcc
    }
    static ObjectFromPath(args*) => Acc.ElementFromPath(args*)
    /**
     * Internal method. Used to get an Acc element returned from an event. 
     * @param hWnd Window/control handle
     * @param idObject Object ID of the object that generated the event
     * @param idChild Specifies whether the event was triggered by an object or one of its child elements.
     * @returns {Acc.IAccessible}
     */
    static ObjectFromEvent(hWnd, idObject, idChild) {
        if (DllCall("oleacc\AccessibleObjectFromEvent"
                , "Ptr", hWnd
                , "UInt", idObject
                , "UInt", idChild
                , "Ptr*", pacc := ComValue(9,0)
                , "Ptr", varChild := Buffer(16)) = 0) {
            return Acc.IAccessible(pacc, NumGet(varChild, 8, "UInt"),  DllCall("GetAncestor", "UInt", hWnd, "UInt", 2))
        }
        throw Error("ObjectFromEvent failed", -1)
    }
    /**
     * Returns the root element (Acc element for the desktop)
     * @returns {Acc.IAccessible}
     */
    static GetRootElement() {
        return Acc.ElementFromHandle(0x10010)
    }
    /**
     * Activates accessibility in a Chromium-based window. 
     * The WM_GETOBJECT message is sent to the Chrome_RenderWidgetHostHWND1 control
     * and the render elements' Name property is accessed. Once the message is sent, the method
     * will wait up to 500ms until accessibility is enabled.
     * For a specific Hwnd, accessibility will only be tried to turn on once, and regardless of
     * whether it was successful or not, later calls of this method with that Hwnd will simply return.
     * @returns True if accessibility was successfully turned on.
     */
    static ActivateChromiumAccessibility(hWnd:="") {
        static activatedHwnds := Map()
		if !IsInteger(hWnd)
			hWnd := WinExist(hWnd)
        if activatedHwnds.Has(hWnd)
            return 1
        activatedHwnds[hWnd] := 1, cHwnd := 0
        try cHwnd := ControlGetHwnd("Chrome_RenderWidgetHostHWND1", hWnd)
        if !cHwnd
            return
        SendMessage(WM_GETOBJECT := 0x003D, 0, 1,, cHwnd)
        try {
            rendererEl := Acc.ElementFromHandle(cHwnd,,False).FindElement({Role:15}, 5)
            _ := rendererEl.Name ; it doesn't work without calling CurrentName (at least in Skype)
        }
        
        waitTime := A_TickCount + 500
        while IsSet(rendererEl) && (A_TickCount < waitTime) {
            try {
                if rendererEl.Value
                    return 1
            }
            Sleep 20
        }
    }
    ; Internal method to query an IAccessible pointer
    static Query(pAcc) {
        oCom := ComObjQuery(pAcc, "{618736e0-3c3d-11cf-810c-00aa00389b71}")
        ObjAddRef(oCom.ptr)
        Try Return ComValue(9, oCom.ptr)
    }
    ; Internal method to get the RoleText from Role integer
    static GetRoleText(nRole) {
        if !IsInteger(nRole) {
            if (Type(nRole) = "String") && (nRole != "")
                return nRole
            throw TypeError("The specified role is not an integer!",-1)
        }
        nRole := Integer(nRole)
        nSize := DllCall("oleacc\GetRoleText", "Uint", nRole, "Ptr", 0, "Uint", 0)
        VarSetStrCapacity(&sRole, nSize+2)
        DllCall("oleacc\GetRoleText", "Uint", nRole, "str", sRole, "Uint", nSize+2)
        Return sRole
    }
    ; Internal method to get the StateText from State integer
    static GetStateText(nState) {
        nSize := DllCall("oleacc\GetStateText"
          , "Uint"	, nState
          , "Ptr" 	, 0
          , "Uint"	, 0)
        VarSetStrCapacity(&sState, nSize+2)
        DllCall("oleacc\GetStateText"
          , "Uint"	, nState
          , "str" 	, sState
          , "Uint"	, nSize+2)
        return sState
    }
    /**
     * Registers an event to the provided callback function.
     * Returns an event handler object, that once destroyed will unhook the event.
     * @param callback The callback function with two mandatory arguments: CallbackFunction(oAcc, EventInfo)
     * @param eventMin One of the Acc.Event constants
     * @param eventMax Optional: one of the Acc.Event constants, which if provided will register 
     *     a range of events from eventMin to eventMax
     * @param PID Optional: Process ID from which to register events. Default is all processes.
     * @returns {Object}
     */
    static RegisterWinEvent(callback, eventMin, eventMax?, PID:=0) {
        if HasMethod(eventMin) ; Legacy support: if eventMin is a method, then the arguments are: event, callback, PID
            PID := eventMax ?? PID, eventMax := callback, callback := eventMin, eventMin := eventMax
        if IsSet(eventMax) && HasMethod(eventMax) ; Legacy support: if eventMax is a method, then the arguments are: eventMin, eventMax, callback, PID
            callbackBuf := eventMax, eventMax := eventMin, eventMin := callback, callback := callbackBuf
        if !IsSet(eventMax)
            eventMax := eventMin
        if Type(eventMin) = "String"
            try eventMin := Acc.Event.%eventMin%
        if Type(eventMax) = "String"
            try eventMax := Acc.Event.%eventMax%
        pCallback := CallbackCreate(this.GetMethod("HandleWinEvent").Bind(this, callback), "F", 7)
        hook := Acc.SetWinEventHook(eventMin, eventMax, pCallback, PID)
        return {__Hook:hook, __Callback:pCallback, __Delete:{ call: (*) => (this.UnhookWinEvent(hook), CallbackFree(pCallback)) }}
    }
    ; Internal method. Calls the callback function after wrapping the IAccessible native object
    static HandleWinEvent(fCallback, hWinEventHook, Event, hWnd, idObject, idChild, dwEventThread, dwmsEventTime) {
        Critical
        try return fCallback(oAcc := Acc.ObjectFromEvent(hWnd, idObject, idChild), {Event:Event, EventThread:dwEventThread, EventTime:dwmsEventTime&0x7FFFFFFF, ControlID:hWnd, WinID:oAcc.wId, ObjId:idObject})
    }
    ; Internal method. Hooks a range of events to a callback function.
    static SetWinEventHook(eventMin, eventMax, pCallback, PID:=0) {
        DllCall("ole32\CoInitialize", "Uint", 0)
        Return DllCall("SetWinEventHook", "Uint", eventMin, "Uint", eventMax, "Uint", 0, "UInt", pCallback, "Uint", PID, "Uint", 0, "Uint", 0)
    }
    ; Internal method. Unhooks a WinEventHook.
    static UnhookWinEvent(hHook) {
        Return DllCall("UnhookWinEvent", "Ptr", hHook)
    }
    /**
     * Returns the Hwnd to a window from a set of screen coordinates
     * @param X Screen X-coordinate
     * @param Y Screen Y-coordinate
     */
	static WindowFromPoint(X, Y) { ; by SKAN and Linear Spoon
		return DllCall("GetAncestor", "UInt", DllCall("user32.dll\WindowFromPoint", "Int64", Y << 32 | (X & 0xFFFFFFFF)), "UInt", 2)
	}
    /**
     * Removes all highlights created by Element.Highlight()
     */
    static ClearHighlights() {
        for _, p in Acc.__HighlightGuis {
            for __, r in p
                r.Destroy()
        }
        Acc.__HighlightGuis := Map()
    }

    ; Internal class: AccViewer code
    class Viewer {
        __New() {
            this.Stored := {mwId:0, FilteredTreeView:Map(), TreeView:Map()}
            this.Capturing := False
            this.gViewer := Gui("AlwaysOnTop Resize","AccViewer")
            this.gViewer.OnEvent("Close", (*) => ExitApp())
            this.gViewer.OnEvent("Size", this.GetMethod("gViewer_Size").Bind(this))
            this.gViewer.Add("Text", "w100", "Window Info").SetFont("bold")
            this.LVWin := this.gViewer.Add("ListView", "h140 w250", ["Property", "Value"])
            this.LVWin.OnEvent("ContextMenu", LV_CopyTextMethod := this.GetMethod("LV_CopyText").Bind(this))
            this.LVWin.ModifyCol(1,60)
            this.LVWin.ModifyCol(2,180)
            for _, v in ["Title", "Text", "Id", "Location", "Class(NN)", "Process", "PID"]
                this.LVWin.Add(,v,"")
            this.gViewer.Add("Text", "w100", "Acc Info").SetFont("bold")
            this.LVProps := this.gViewer.Add("ListView", "h220 w250", ["Property", "Value"])
            this.LVProps.OnEvent("ContextMenu", LV_CopyTextMethod)
            this.LVProps.ModifyCol(1,100)
            this.LVProps.ModifyCol(2,140)
            for _, v in ["RoleText", "Role", "Value", "Name", "Location", "StateText", "State", "DefaultAction", "Description", "KeyboardShortcut", "Help", "ChildId"]
                this.LVProps.Add(,v,"")
            this.ButCapture := this.gViewer.Add("Button", "xp+60 y+10 w130", "Start capturing (F1)")
            this.ButCapture.OnEvent("Click", this.CaptureHotkeyFunc := this.GetMethod("ButCapture_Click").Bind(this))
            HotKey("~F1", this.CaptureHotkeyFunc)
            this.SBMain := this.gViewer.Add("StatusBar",, "  Start capturing, then hold cursor still to construct tree")
            this.SBMain.OnEvent("Click", this.GetMethod("SBMain_Click").Bind(this))
            this.SBMain.OnEvent("ContextMenu", this.GetMethod("SBMain_Click").Bind(this))
            this.gViewer.Add("Text", "x278 y10 w100", "Acc Tree").SetFont("bold")
            this.TVAcc := this.gViewer.Add("TreeView", "x275 y25 w250 h390 -0x800")
            this.TVAcc.OnEvent("Click", this.GetMethod("TVAcc_Click").Bind(this))
            this.TVAcc.OnEvent("ContextMenu", this.GetMethod("TVAcc_ContextMenu").Bind(this))
            this.TVAcc.Add("Start capturing to show tree")
            this.TextFilterTVAcc := this.gViewer.Add("Text", "x275 y428", "Filter:")
            this.EditFilterTVAcc := this.gViewer.Add("Edit", "x305 y425 w100")
            this.EditFilterTVAcc.OnEvent("Change", this.GetMethod("EditFilterTVAcc_Change").Bind(this))
            this.gViewer.Show()
        }
        ; Resizes window controls when window is resized
        gViewer_Size(GuiObj, MinMax, Width, Height) {
            this.TVAcc.GetPos(&TVAccX, &TVAccY, &TVAccWidth, &TVAccHeight)
            this.TVAcc.Move(,,Width-TVAccX-10,Height-TVAccY-50)
            this.TextFilterTVAcc.Move(TVAccX, Height-42)
            this.EditFilterTVAcc.Move(TVAccX+30, Height-45)
            this.TVAcc.GetPos(&LVPropsX, &LVPropsY, &LVPropsWidth, &LVPropsHeight)
            this.LVProps.Move(,,,Height-LVPropsY-225)
            this.ButCapture.Move(,Height -55)
        }
        ; Starts showing the element under the cursor with 200ms intervals with CaptureCallback
        ButCapture_Click(GuiCtrlObj?, Info?) {
            if this.Capturing {
                this.StopCapture()
                return
            }
            this.Capturing := True
            HotKey("~F1", this.CaptureHotkeyFunc, "Off")
            HotKey("~Esc", this.CaptureHotkeyFunc, "On")
            this.TVAcc.Delete()
            this.TVAcc.Add("Hold cursor still to construct tree")
            this.ButCapture.Text := "Stop capturing (Esc)"
            this.CaptureCallback := this.GetMethod("CaptureCycle").Bind(this)
            SetTimer(this.CaptureCallback, 200)
        }
        ; Handles right-clicking a listview (copies to clipboard)
        LV_CopyText(GuiCtrlObj, Info, *) {
            LVData := Info > GuiCtrlObj.GetCount()
                ? ListViewGetContent("", GuiCtrlObj)
                : ListViewGetContent("Selected", GuiCtrlObj)
            ToolTip("Copied: " (A_Clipboard := RegExReplace(LVData, "([ \w]+)\t", "$1: ")))
            SetTimer((*) => ToolTip(), -3000)
        }
        ; Copies the Acc path to clipboard when statusbar is clicked
        SBMain_Click(GuiCtrlObj, Info, *) {
            if InStr(this.SBMain.Text, "Path:") {
                ToolTip("Copied: " (A_Clipboard := SubStr(this.SBMain.Text, 9)))
                SetTimer((*) => ToolTip(), -3000)
            }
        }
        ; Stops capturing elements under mouse, unhooks CaptureCallback
        StopCapture(GuiCtrlObj:=0, Info:=0) {
            if this.Capturing {
                this.Capturing := False
                this.ButCapture.Text := "Start capturing (F1)"
                HotKey("~Esc", this.CaptureHotkeyFunc, "Off")
                HotKey("~F1", this.CaptureHotkeyFunc, "On")
                SetTimer(this.CaptureCallback, 0)
                this.Stored.oAcc.Highlight()
                return
            }
        }
        ; Gets Acc element under mouse, updates the GUI. 
        ; If the mouse is not moved for 1 second then constructs the Acc tree.
        CaptureCycle() {
            MouseGetPos(&mX, &mY, &mwId)
            oAcc := Acc.ElementFromPoint()
            if this.Stored.HasOwnProp("oAcc") && IsObject(oAcc) && oAcc.IsEqual(this.Stored.oAcc) {
                if this.FoundTime != 0 && ((A_TickCount - this.FoundTime) > 1000) {
                    if (mX == this.Stored.mX) && (mY == this.Stored.mY) 
                        this.ConstructTreeView(), this.FoundTime := 0
                    else 
                        this.FoundTime := A_TickCount
                }
                this.Stored.mX := mX, this.Stored.mY := mY
                return
            }
            this.LVWin.Delete()
            WinGetPos(&mwX, &mwY, &mwW, &mwH, mwId)
            propsOrder := ["Title", "Text", "Id", "Location", "Class(NN)", "Process", "PID"]
            props := Map("Title", WinGetTitle(mwId), "Text", WinGetText(mwId), "Id", mwId, "Location", "x: " mwX " y: " mwY " w: " mwW " h: " mwH, "Class(NN)", WinGetClass(mwId), "Process", WinGetProcessName(mwId), "PID", WinGetPID(mwId))
            for propName in propsOrder
                this.LVWin.Add(,propName,props[propName])
            this.LVProps_Populate(oAcc)
            this.Stored.mwId := mwId, this.Stored.oAcc := oAcc, this.Stored.mX := mX, this.Stored.mY := mY, this.FoundTime := A_TickCount
        }
        ; Populates the listview with Acc element properties
        LVProps_Populate(oAcc) {
            Acc.ClearHighlights() ; Clear
            oAcc.Highlight(0) ; Indefinite show
            this.LVProps.Delete()
            Location := {x:"N/A",y:"N/A",w:"N/A",h:"N/A"}, RoleText := "N/A", Role := "N/A", Value := "N/A", Name := "N/A", StateText := "N/A", State := "N/A", DefaultAction := "N/A", Description := "N/A", KeyboardShortcut := "N/A", Help := "N/A", ChildId := ""
            for _, v in ["RoleText", "Role", "Value", "Name", "Location", "StateText", "State", "DefaultAction", "Description", "KeyboardShortcut", "Help", "ChildId"] {
                try %v% := oAcc.%v%
                this.LVProps.Add(,v, v = "Location" ? ("x: " %v%.x " y: " %v%.y " w: " %v%.w " h: " %v%.h) : %v%)
            }
        }
        ; Handles selecting elements in the Acc tree, highlights the selected element
        TVAcc_Click(GuiCtrlObj, Info) {
            if this.Capturing
                return
            try oAcc := this.EditFilterTVAcc.Value ? this.Stored.FilteredTreeView[Info] : this.Stored.TreeView[Info]
            if IsSet(oAcc) && oAcc {
                try this.SBMain.SetText("  Path: " oAcc.Path)
                this.LVProps_Populate(oAcc)
            }
        }
        ; Permits copying the Dump of Acc element(s) to clipboard
        TVAcc_ContextMenu(GuiCtrlObj, Item, IsRightClick, X, Y) {
            TVAcc_Menu := Menu()
            try oAcc := this.EditFilterTVAcc.Value ? this.Stored.FilteredTreeView[Item] : this.Stored.TreeView[Item]
            if IsSet(oAcc)
                TVAcc_Menu.Add("Copy to Clipboard", (*) => A_Clipboard := oAcc.Dump())
            TVAcc_Menu.Add("Copy Tree to Clipboard", (*) => A_Clipboard := Acc.ElementFromHandle(this.Stored.mwId).DumpAll())
            TVAcc_Menu.Show()
        }
        ; Handles filtering the Acc elements inside the TreeView when the text hasn't been changed in 500ms.
        ; Sorts the results by Acc properties.
        EditFilterTVAcc_Change(GuiCtrlObj, Info, *) {
            static TimeoutFunc := "", ChangeActive := False
            if !this.Stored.TreeView.Count
                return
            if (Info != "DoAction") || ChangeActive {
                if !TimeoutFunc
                    TimeoutFunc := this.GetMethod("EditFilterTVAcc_Change").Bind(this, GuiCtrlObj, "DoAction")
                SetTimer(TimeoutFunc, -500)
                return
            }
            ChangeActive := True
            this.Stored.FilteredTreeView := Map(), parents := Map()
            if !(searchPhrase := this.EditFilterTVAcc.Value) {
                this.ConstructTreeView()
                ChangeActive := False
                return
            }
            this.TVAcc.Delete()
            temp := this.TVAcc.Add("Searching...")
            Sleep -1
            this.TVAcc.Opt("-Redraw")
            this.TVAcc.Delete()
            for index, oAcc in this.Stored.TreeView {
                for _, prop in ["RoleText", "Role", "Value", "Name", "StateText", "State", "DefaultAction", "Description", "KeyboardShortcut", "Help", "ChildId"] {
                    try {
                        if InStr(oAcc.%Prop%, searchPhrase) {
                            if !parents.Has(prop)
                                parents[prop] := this.TVAcc.Add(prop,, "Expand")
                            this.Stored.FilteredTreeView[this.TVAcc.Add(this.GetShortDescription(oAcc), parents[prop], "Expand")] := oAcc
                        }
                    }
                }
            }
            if !this.Stored.FilteredTreeView.Count
                this.TVAcc.Add("No results found matching `"" searchPhrase "`"")
            this.TVAcc.Opt("+Redraw")
            TimeoutFunc := "", ChangeActive := False
        }
        ; Populates the TreeView with the Acc tree when capturing and the mouse is held still
        ConstructTreeView() {
            this.TVAcc.Delete()
            this.TVAcc.Add("Constructing Tree, please wait...")
            Sleep -1
            this.TVAcc.Opt("-Redraw")
            this.TVAcc.Delete()
            this.Stored.TreeView := Map()
            this.RecurseTreeView(Acc.ElementFromHandle(this.Stored.mwId))
            this.TVAcc.Opt("+Redraw")
            for k, v in this.Stored.TreeView
                if this.Stored.oAcc.IsEqual(v)
                    this.TVAcc.Modify(k, "Vis Select"), this.SBMain.SetText("  Path: " v.Path)
        }
        ; Stores the Acc tree with corresponding path values for each element
        RecurseTreeView(oAcc, parent:=0, path:="") {
            this.Stored.TreeView[TWEl := this.TVAcc.Add(this.GetShortDescription(oAcc), parent, "Expand")] := oAcc.DefineProp("Path", {value:path})
            for k, v in oAcc
                this.RecurseTreeView(v, TWEl, path (path?",":"") k)
        }
        ; Creates a short description string for the Acc tree elements
        GetShortDescription(oAcc) {
            elDesc := " `"`""
            try elDesc := " `"" oAcc.Name "`""
            try elDesc := oAcc.RoleText elDesc
            catch
                elDesc := "`"`"" elDesc
            return elDesc
        }
    }
}
Change history:

Code: Select all

29.08.22: Added ControlId property. 
RoleText handles Role being a string instead of an integer (even though the documentation states it should always be one of the ROLE constants).
30.08.22: Added Normalize method. Fixed Parent, fixed not-condition being improperly handled.
31.08.22: Added support for conditions in __Item path, which also supports "index" to specify which element to select from multiple. Added scope argument to Dump(), and NoActivate argument to Click().
01.09.22: Click() no longer dependant on CoordMode, and if the clickable point in the window is not visible then activates the window before clicking.
04.09.22: Fixed Highlight() sometimes throwing an error if Location isn't available. Improved IsEqual. Added filtering to Acc Tree. Added right-click copy element/tree to Acc Tree.
05.09.22: Added indexed matching to FindFirst (ability to find nth match and to reverse search direction).
20.11.22: Fixed RecurseTreeView second arguments default value.
08.12.22: Fixed ValidateCondition by Location, also now can match relative to client/window/screen; N/A can be matched by using empty object {}; added True and False conditions. Changed Dump to report N/A values if no defined Location. FindAll by default matches True condition.
10.12.22: Added GetPath to get the Acc path to a sub-element. Changed WaitElementExist to WaitElement, added WaitElementExist that checks whether Element.Exists is true (which means the element is visible and accessible). __Item now supports negative indexes. Changed default hWnds to Last Found Window (to be more consistent with AHK overall). Added ObjectFromChromium. Multiple smaller bug fixes. Improved comments.
11.12.22: Added optional eventMax argument to RegisterWinEvent to allow an event range to be registered. ObjectFromPath allows combining RoleText with index (negative indexes not supported). Added max recursion depth argument to Find and Dump methods.
17.12.22: Added PID argument to RegisterWinEvent. Modified the event callback function return values to return more useful information.

Update to v2.1 on 14.01.23: 
Breaking changes: WaitElement and WaitElementExist timeOut and scope arguments have been switched. FindElement(s) depth argument has been pushed back by index and order arguments. Dump and DumpAll delimiter argument added before the depth argument. RegisterWinEvent callback argument moved to the front.
Other changes: methods names have been changed to more closely follow UIA syntax: ObjectFromWindow -> ElementFromHandle, ObjectFromPoint -> ElementFromPoint, FindFirst -> FindElement, FindAll -> FindElements.
order argument (which can be one of Acc.TreeTraversalOptions values) has been to traverse the tree PostOrder or LastToFirst.
index argument is now separate from the condition object.
Added ObjId to RegisterWinEvent callback functions EventInfo object.
26.01.23: Fixed WindowFromPoint with negative coordinates.
Last edited by Descolada on 26 Jan 2023, 00:20, edited 26 times in total.

BoBo
Posts: 6564
Joined: 13 May 2014, 17:15

Re: Acc v2

Post by BoBo » 29 Aug 2022, 05:23

That's great! Thank you so much for your contributions :thumbup:
Question, what's the benefit of using ACC over UIA or vice versa (so people can decide which one to prefer based on their environment/requirements)?

8-)

Descolada
Posts: 1123
Joined: 23 Dec 2021, 02:30

Re: Acc v2

Post by Descolada » 29 Aug 2022, 05:53

@BoBo, that's a great question! Generally I view Acc v2 as a gateway-drug to UIA: it is a simpler, smaller, and (arguably) easier to use, but the ease of use comes at the expense of functionality.

Pros of Acc
1) Ease of use
2) Faster than UIA (except in FindFirst/FindAll methods)
3) Can get the caret position (I haven't figured out a way of how to do that with UIA)

Pros of UIA
1) UIA offers WAY more properties than Acc, which means it also has more ways of finding/separating elements
2) UIA is faster and more flexible in finding elements, whereas Acc is super fast in getting elements by path
3) UIA has more ways of interacting with elements (selecting items, toggling etc)
4) UIA can sometimes be used without the window in focus, with Acc you can't interact with it unless it is in focus

neogna2
Posts: 590
Joined: 15 Sep 2016, 15:44

Re: Acc v2

Post by neogna2 » 30 Aug 2022, 17:43

Great work Descolada! I'm thrilled to try out this a lot more.

One more difference between your v2 Acc to and v1 Acc (and the other v2 Acc attempts): the others allow path segments that are either numbers or a role text string. Some examples.

Code: Select all

AccPath := "4.14.1.menu_item" ;first child object with role "menu item"
AccPath := "4.14.1.menu_item1" ;equivalent to the above
AccPath := "4.14.1.menu_item2" ;second
AccPath := "4.14.1.menu_item3.1.4.4" ; third child, then its child, grandchild, ...
I found that useful in windows where an element's path changes depending on if other UI elements are toggled visible or not (for example if a sidebar is open or not). In some such cases using a role text string for a segment could yield a stable path. For example "4.14.1.menu_item" could keep working whereas the numerical segment path flips from "4.14.1.2" to "4.14.1.3". But perhaps the other features (like FindFirst) you've added to your v2 Acc can let us handle such cases in some other way...
Descolada wrote:
29 Aug 2022, 05:53
4) UIA can sometimes be used without the window in focus, with Acc you can't interact with it unless it is in focus
Not sure what you mean by interact there. But we can use your v2 Acc to for example update the text in an inactive AutoHotkey GUI window's edit control. But oAcc.Click() on the other hand does not click the oAcc window if inactive, but instead clicks in whatever other window is active, which can be surprising.

Descolada
Posts: 1123
Joined: 23 Dec 2021, 02:30

Re: Acc v2

Post by Descolada » 30 Aug 2022, 23:25

@neogna2,
One more difference between your v2 Acc to and v1 Acc (and the other v2 Acc attempts): the others allow path segments that are either numbers or a role text string. Some examples.
Excellent idea! I didn't fully implement this since I want to think a little bit how to handle getting the nth item matching a condition, but for now it's possible to do oAcc[4,{Role:10}]. For now, getting an nth item would require a bit more code:

Code: Select all

oAcc[4].FindAll({RoleText:"menu item"}, 2)[2] ; gets second child with RoleText of "menu item"
But we can use your v2 Acc to for example update the text in an inactive AutoHotkey GUI window's edit control. But oAcc.Click() on the other hand does not click the oAcc window if inactive, but instead clicks in whatever other window is active, which can be surprising.
Yes, setting an edit control without focusing the window is possible in some cases, but DoDefaultAction isn't. With UIA it's often possible to use Invoke, Select etc without focusing.
Indeed oAcc.Click() doesn't focus the target window. I was thinking that perhaps there may be situations where the user doesn't want to activate the window before clicking? For example if activating the window causes some pop-up or causes a menu to close. But I'm not sure about this... I could make it an optional argument though.

EDIT: it's now possible to also specify an index inside the condition: oAcc[4, {RoleText:"menu item", i:2}] selects the 4th childs second menu item element. Index can also be negative to select from the end.

neogna2
Posts: 590
Joined: 15 Sep 2016, 15:44

Re: Acc v2

Post by neogna2 » 31 Aug 2022, 16:49

Descolada wrote:
30 Aug 2022, 23:25
EDIT: it's now possible to also specify an index inside the condition: oAcc[4, {RoleText:"menu item", i:2}] selects the 4th childs second menu item element. Index can also be negative to select from the end.
Nice! I will test it and get back to you if I run into any issues.
Descolada wrote:
30 Aug 2022, 23:25
Indeed oAcc.Click() doesn't focus the target window. I was thinking that perhaps there may be situations where the user doesn't want to activate the window before clicking? For example if activating the window causes some pop-up or causes a menu to close.
Ok, that makes sense. Though even if oAcc.Click() doesn't activate the oAcc window it could still be changed to check if that window is already active and if not then do nothing. (Or could such a check also affect a meny or some other GUI element?) Currently if some other window is active oAcc.Click will trigger a click in that other window and if that really was the user's intended outcome it seems less confusing to instead use (non-Acc) Click or ControlClick.
Last edited by neogna2 on 01 Sep 2022, 15:22, edited 1 time in total.

AHK_user
Posts: 515
Joined: 04 Dec 2015, 14:52
Location: Belgium

Re: Acc v2

Post by AHK_user » 01 Sep 2022, 05:27

I noticed you added an example regarding the VSCode_LineColumn, something I was trying to figure out with the other acc library. :D

When I have the time, I will rewrite it to create an usefull help hotkey to search for the command where the cursor is located.

And I will probably also add an acc viewer to the Winspector tool, if I find the time :think: .

Good work, thanks a lot!

AHK_user
Posts: 515
Joined: 04 Dec 2015, 14:52
Location: Belgium

Re: Acc v2

Post by AHK_user » 01 Sep 2022, 12:03

Maybe a small suggestion/idea, would it not be better to call this library oAcc.ahk to avoid confusion with the older version?

Descolada
Posts: 1123
Joined: 23 Dec 2021, 02:30

Re: Acc v2

Post by Descolada » 01 Sep 2022, 13:57

@AHK_user, I did consider naming it DAcc, but since there didn't appear to be any good/complete ports of the original Acc library for AHK v2 then I used the chance to be the first and grabbed the "original" name ;) Whether its better to change the name now, I don't know, but in my opinion if AHK v2 kept the original name in spite of code-breaking changes, then Acc v2 could keep the name of the original as well. I suppose if a good port of the "original" Acc comes along and supersedes mine in popularity then I'll make the change...

neogna2
Posts: 590
Joined: 15 Sep 2016, 15:44

Re: Acc v2

Post by neogna2 » 01 Sep 2022, 15:26

I like many of the changes in this @Descolada version, but best case scenario (for v2 growth and helping people migrate) might be to have both this Acc.ahk and then Acc_legacy.ahk (or something like that) which is more of a v2 drop in replacement for v1 Acc.

AHK_user
Posts: 515
Joined: 04 Dec 2015, 14:52
Location: Belgium

Re: Acc v2

Post by AHK_user » 04 Sep 2022, 08:30

I have made a simple alternative viewer for the acc data. The included viewer in acc.ahk is amazingly made, but I am missing a search function to search text in all elements of a window.

It actually just puts the dumpall data in a searchable list.
This way it is very easy to analyse the acc data of a window.

I am still looking for a good way to the the correct path after getting the acc element from ObjectFromPoint, so I can select it in the listview.
Does anyone knows a good way?


EDITED:
- Added autoselect the picked element.
- Moved name of element more to front
- Added missing parameters
AccListViewer.png
AccListViewer.png (37.52 KiB) Viewed 7421 times

Code: Select all

#include ..\Lib\Acc.ahk
#Requires AutoHotkey v2.0-a
#SingleInstance Force

; Acc List viewer to quickly see all acc data from a specified window

; Create the window:
myGui := Gui(,"Acc Viewer")
myGui.Opt("+Resize")
myGui.OnEvent("Size", Gui_Size)
myGui.OnEvent("Close", (*)=>(ExitApp))
ogButton_Selector := MyGui.addButton("xm y2 w60 vbtnSelector BackgroundTrans h24 w24 +0x4000", "+")
ogButton_Selector.SetFont("s20", "Times New Roman")
ogButton_Selector.statusbar := "Click and drag to select a specific control or window"

; Create the ListView with two columns, Name and Size:
ogEditSearch := myGui.AddText("ym x+10","Search:")
ogEditSearch := myGui.AddEdit("yp-2 x+10")
ogEditSearch.OnEvent("Change",(*)=>(LV_Update()))

LV := myGui.Add("ListView", "xm yp+24 r25 w800", ["Path","Name","RoleText","Role","x","y","w","h","Value", "StateText", "State", "Description", "KeyboardShortcut", "Help", "ChildId"])
LV.OnEvent("ContextMenu", LV_ContextMenu)
; Notify the script whenever the user double clicks a row:
LV.OnEvent("DoubleClick", LV_DoubleClick)

SB := MyGui.AddStatusBar(,)
LV_Update("A")
LV.ModifyCol  ; Auto-size each column to fit its contents.
LV.ModifyCol(2, "Integer")  ; For sorting purposes, indicate that column 2 is an integer.
OnMessage(WM_LBUTTONDOWN := 0x0201, CheckButtonClick)
MyGui.Show

LV_Update(WinTitle:=""){
    global Acc_Content
    
    LV.Delete()
    SearchText := ogEditSearch.text
    LV.Opt("-Redraw")
    SB.SetText("Reading acc data...")

    if (WinTitle != "") {
        Title := WinGetTitle(Wintitle)
        myGui.Title := "Acc Viewer - [" Title "]"
        oAcc := Acc.ObjectFromWindow(WinTitle)
        global Acc_Content := oAcc.DumpAll()
        myGui.WinTitle := Wintitle
    }

    SB.SetText("Generating list...")

    Counter := 0
    CounterTotal:=0

    Loop Parse, Acc_Content, "`n","`r"
        {
            ; 4,1: RoleText: pane Role: 16 [Location: {x:3840,y:0,w:3840,h:2100}] [Name: ] [Value: ] [StateText: normal]
            CounterTotal++
            Path := RegExReplace(A_LoopField,"^([\d,]*):.*","$1", &OutputVarCount )
            Path := OutputVarCount=0 ? "" : Path
            RoleText := RegExReplace(A_LoopField,".*\QRoleText: \E(.*)\Q Role: \E.*","$1")
            Role := RegExReplace(A_LoopField,".*\Q Role: \E(.*)\Q [Location: \E.*","$1")
            x := RegExReplace(A_LoopField,".*\Q [Location: {x:\E(.*)\Q,y:\E.*","$1")
            y := RegExReplace(A_LoopField,".*\Q,y:\E(.*)\Q,w:\E.*","$1")
            w := RegExReplace(A_LoopField,".*\Q,w:\E(.*)\Q,h:\E.*","$1")
            h := RegExReplace(A_LoopField,".*\Q,h:\E(.*)\Q}] \E.*","$1")
            name := RegExReplace(A_LoopField,".*\Q}] [Name:\E(.*?)\Q] [\E.*","$1", &OutputVarCount)
            name := OutputVarCount=0 ? "" : name
            value := RegExReplace(A_LoopField,".*\Q] [Value:\E(.*?)\Q] [\E.*","$1", &OutputVarCount)
            value := OutputVarCount=0 ? "" : value
            description := RegExReplace(A_LoopField,".*\Q] [Description: \E(.*?)\Q]\E.*","$1", &OutputVarCount)
            description := OutputVarCount=0 ? "" : description
            StateText := RegExReplace(A_LoopField,".*\Q] [StateText: \E(.*?)(\Q] [\E.*|\Q]\E)$","$1", &OutputVarCount)
            StateText := OutputVarCount=0 ? "" : StateText
            State := RegExReplace(A_LoopField,".*\Q] [State: \E(.*?)(\Q] [\E.*|\Q]\E)$","$1", &OutputVarCount)
            State := OutputVarCount=0 ? "" : State
            KeyboardShortcut := RegExReplace(A_LoopField,".*\Q] [KeyboardShortcut: \E(.*?)(\Q] [\E.*|\Q]\E)$","$1", &OutputVarCount)
            KeyboardShortcut := OutputVarCount=0 ? "" : KeyboardShortcut
            Help := RegExReplace(A_LoopField,".*\Q] [Help: \E(.*?)(\Q] [\E.*|\Q]\E)$","$1", &OutputVarCount)
            Help := OutputVarCount=0 ? "" : Help
            ChildId := RegExReplace(A_LoopField,".*\Q ChildId: \E(.*?)(\Q [\E.*|\Q\E)$","$1", &OutputVarCount)
            ChildId := OutputVarCount=0 ? "" : ChildId

            if (ogEditSearch.text != "" and !InStr(Path "." RoleText "." Role "." Name "." value "." Description "." StateText "." State "." KeyboardShortcut "." Help ,ogEditSearch.text )){
                continue
            }
            RowNumber := LV.Add(, Path, name, RoleText, Role, x, y, w, h,  value, StateText, State, Description, KeyboardShortcut, Help, ChildId)
            if (myGui.HasProp("ElID") and myGui.ElID = x "-" y "-" w "-" h "-" Role){
                LV.Modify(RowNumber, "Select Focus Vis")
            }
            Counter++
        }

    SB.SetText((Counter=Countertotal) ? "Found " Counter " elements." : "Filtered " Counter "/" CounterTotal)
    LV.Opt("+Redraw")
}

LV_DoubleClick(LV, RowNumber){
    RowText := LV.GetText(RowNumber)  ; Get the text from the row's first field.
    ; ToolTip("You double-clicked row number " RowNumber ". Text: '" RowText "'")
    ChildPath := LV.GetText(RowNumber)
    oAccp := Acc.ObjectFromPath(ChildPath, myGui.WinTitle)
    oAccp.Highlight(0)
}

Gui_Size(thisGui, MinMax, Width, Height) {
    if MinMax = -1	; The window has been minimized. No action needed.
        return
    DllCall("LockWindowUpdate", "Uint", thisGui.Hwnd)
    LV.GetPos(&cX, &cY, &cWidth, &cHeight)
    LV.Move(, , Width - cX - 10, Height -cY -26)
    DllCall("LockWindowUpdate", "Uint", 0)
}

LV_ContextMenu(LV, RowNumber, IsRightClick, X, Y){
    RowNumber := 0  ; This causes the first loop iteration to start the search at the top of the list.
    Counter:=0
    Loop{
        RowNumber := LV.GetNext(RowNumber)  ; Resume the search at the row after that found by the previous iteration.
        if not RowNumber
            break
        Counter++
    }
    if (Counter=1){
        path := LV.GetText(RowNumber)
        MyMenu := Menu()
        MyMenu.add "Copy Path", (*) =>(A_Clipboard :=path, Tooltip2("Copied [" A_Clipboard "]"))
        MyMenu.Show
    }

}

CheckButtonClick(wParam := 0, lParam := 0, msg := 0, hwnd := 0) {
    global MyGui
    MouseGetPos(, , , &OutputVarControlHwnd, 2)

    if (ogButton_Selector.hwnd = OutputVarControlHwnd) {
        ogButton_Selector.text := ""
        SetSystemCursor("Cross")
        CoordMode "Mouse", "Screen"
        While (GetKeyState("LButton")) {
            MouseGetPos(&MouseX, &MouseY, &MouseWinHwnd, &MouseControlHwnd, 2)
            Sleep(100)
            if ( MouseControlHwnd != "") {
                MouseGetPos(&MouseX, &MouseY, &MouseWinHwnd, &MouseControlHwnd, 2)
                oAccp := Acc.ObjectFromPoint(MouseX, MouseY)
                myGui.oAccp := oAccp
                myGui.ElID := oAccp.location.x "-" oAccp.location.y "-" oAccp.location.w "-" oAccp.location.h "-" oAccp.Role
                oAccp.Highlight(0)
            }
        }
 
        SetSystemCursor("Default")
        LV_Update("ahk_id" MouseWinHwnd)
        ogButton_Selector.text := "+"
    }
}

Tooltip2(Text := "", X := "", Y := "", WhichToolTip := "") {
    ToolTip(Text, X, Y, WhichToolTip)
    SetTimer () => ToolTip(), -3000
}

SetSystemCursor(Cursor := "", cx := 0, cy := 0) {

    if (Cursor = "Default") {
        return DllCall("SystemParametersInfo", "UInt", SPI_SETCURSORS := 0x57, "UInt", 0, "UInt", 0, "UInt", 0)
    }

    static SystemCursors := Map("APPSTARTING", 32650, "ARROW", 32512, "CROSS", 32515, "HAND", 32649, "HELP", 32651, "IBEAM", 32513, "NO", 32648,
        "SIZEALL", 32646, "SIZENESW", 32643, "SIZENS", 32645, "SIZENWSE", 32642, "SIZEWE", 32644, "UPARROW", 32516, "WAIT", 32514)

    if (Cursor = "") {
        AndMask := Buffer(128, 0xFF), XorMask := Buffer(128, 0)

        for CursorName, CursorID in SystemCursors {
            CursorHandle := DllCall("CreateCursor", "ptr", 0, "int", 0, "int", 0, "int", 32, "int", 32, "ptr", AndMask, "ptr", XorMask, "ptr")
            DllCall("SetSystemCursor", "ptr", CursorHandle, "int", CursorID)	; calls DestroyCursor
        }
        return
    }

    if (Cursor ~= "^(IDC_)?(?i:AppStarting|Arrow|Cross|Hand|Help|IBeam|No|SizeAll|SizeNESW|SizeNS|SizeNWSE|SizeWE|UpArrow|Wait)$") {
        Cursor := RegExReplace(Cursor, "^IDC_")

        if !(CursorShared := DllCall("LoadCursor", "ptr", 0, "ptr", SystemCursors[StrUpper(Cursor)], "ptr"))
            throw Error("Error: Invalid cursor name")

        for CursorName, CursorID in SystemCursors {
            CursorHandle := DllCall("CopyImage", "ptr", CursorShared, "uint", 2, "int", cx, "int", cy, "uint", 0, "ptr")
            DllCall("SetSystemCursor", "ptr", CursorHandle, "int", CursorID)	; calls DestroyCursor
        }
        return
    }

    if FileExist(Cursor) {
        SplitPath Cursor, , , &Ext := ""	; auto-detect type
        if !(uType := (Ext = "ani" || Ext = "cur") ? 2 : (Ext = "ico") ? 1 : 0)
            throw Error("Error: Invalid file type")

        if (Ext = "ani") {
            for CursorName, CursorID in SystemCursors {
                CursorHandle := DllCall("LoadImage", "ptr", 0, "str", Cursor, "uint", uType, "int", cx, "int", cy, "uint", 0x10, "ptr")
                DllCall("SetSystemCursor", "ptr", CursorHandle, "int", CursorID)	; calls DestroyCursor
            }
        } else {
            if !(CursorShared := DllCall("LoadImage", "ptr", 0, "str", Cursor, "uint", uType, "int", cx, "int", cy, "uint", 0x8010, "ptr"))
                throw Error("Error: Corrupted file")

            for CursorName, CursorID in SystemCursors {
                CursorHandle := DllCall("CopyImage", "ptr", CursorShared, "uint", 2, "int", 0, "int", 0, "uint", 0, "ptr")
                DllCall("SetSystemCursor", "ptr", CursorHandle, "int", CursorID)	; calls DestroyCursor
            }
        }
        return
    }

    throw Error("Error: Invalid file path or cursor name")
}
Last edited by AHK_user on 04 Sep 2022, 12:27, edited 4 times in total.

Descolada
Posts: 1123
Joined: 23 Dec 2021, 02:30

Re: Acc v2

Post by Descolada » 04 Sep 2022, 11:04

@AHK_user, nice! I miss a good search function as well, but implementing it in AccViewer seemed a bit of a pain (it should replace the treeview with a listview or something?). I think I will add the DumpAll to clipboard option to it though, easy enough to paste into Notepad.
I am still looking for a good way to the the correct path after getting the acc element from ObjectFromPoint, so I can select it in the listview.
My implementation of element.IsEqual(oCompare) just checks if the location, size and role for both elements are the same - I think it would be easy to implement that in your script as well.

Btw, there might be more content in Dump() than your example of ; 4,1: RoleText: pane Role: 16 [Location: {x:3840,y:0,w:3840,h:2100}] [Name: ] [Value: ] [StateText: normal] contains. Namely Dump/DumpAll 100% displays RoleText, Role, Location, Name and Value, but others are displayed only if they have any content (otherwise there would be LOTS of empty data). The full list is RoleText, Role, Value, Name, StateText, State, DefaultAction, Description, KeyboardShortcut, Help, ChildId (this one without brackets).

BoBo
Posts: 6564
Joined: 13 May 2014, 17:15

Re: Acc v2

Post by BoBo » 04 Sep 2022, 11:11

@Descolada - adding a button to the viewer that gets active once that additional script/module is present at the viewers root directory? :think:

AHK_user
Posts: 515
Joined: 04 Dec 2015, 14:52
Location: Belgium

Re: Acc v2

Post by AHK_user » 04 Sep 2022, 12:37

Descolada wrote:
04 Sep 2022, 11:04
@AHK_user, nice! I miss a good search function as well, but implementing it in AccViewer seemed a bit of a pain (it should replace the treeview with a listview or something?). I think I will add the DumpAll to clipboard option to it though, easy enough to paste into Notepad.
I am still looking for a good way to the the correct path after getting the acc element from ObjectFromPoint, so I can select it in the listview.
My implementation of element.IsEqual(oCompare) just checks if the location, size and role for both elements are the same - I think it would be easy to implement that in your script as well.

Btw, there might be more content in Dump() than your example of ; 4,1: RoleText: pane Role: 16 [Location: {x:3840,y:0,w:3840,h:2100}] [Name: ] [Value: ] [StateText: normal] contains. Namely Dump/DumpAll 100% displays RoleText, Role, Location, Name and Value, but others are displayed only if they have any content (otherwise there would be LOTS of empty data). The full list is RoleText, Role, Value, Name, StateText, State, DefaultAction, Description, KeyboardShortcut, Help, ChildId (this one without brackets).
@Descolada: I added the selecting of the picked element, the only disadvantage is that sometimes there are multiple rows picked, but it is close enough.

One known possible error that may occur can happen if some parameters contain brackets.

I have added some missing parameters.
Feel free to copy the cross Drag-Drop functionality if you like it, I personally find it more intuïtive than permanent monitoring.

This accList version is added to my wInspector tool, in the context menu of the winlist. I kept it as a separate gui for now.

Descolada
Posts: 1123
Joined: 23 Dec 2021, 02:30

Re: Acc v2

Post by Descolada » 04 Sep 2022, 13:55

AHK_user wrote:
04 Sep 2022, 12:37
I added the selecting of the picked element, the only disadvantage is that sometimes there are multiple rows picked, but it is close enough.
Thanks, this helped me discover a bug in IsEqual. Invisible elements usually have location 0;0 and size 0;0, and the Role may easily match, so I added a more thorough comparison in that case.

I also tried to implement a filter function in AccViewer, also added right-click copy to Acc Tree.

malcev
Posts: 1769
Joined: 12 Aug 2014, 12:37

Re: Acc v2

Post by malcev » 11 Sep 2022, 06:21

Descolada wrote:
29 Aug 2022, 05:53
3) Can get the caret position (I haven't figured out a way of how to do that with UIA)
In uia we only have
https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextpattern2-getcaretrange
Something like this:

Code: Select all

f11::
IUIAutomation := ComObjCreate(CLSID_CUIAutomation8 := "{e22ad333-b25f-460c-83d0-0581107395c9}", IID_IUIAutomation := "{30cbe57d-d9d0-452a-ab13-7ac5ac4825ee}")
DllCall(NumGet(NumGet(IUIAutomation+0)+7*A_PtrSize), "ptr", IUIAutomation, "int64", 0*DllCall("GetCursorPos","Int64*",pt)+pt, "ptr*", ElementFromPoint)   ; IUIAutomation::ElementFromPoint
DllCall(NumGet(NumGet(ElementFromPoint+0)+16*A_PtrSize), "ptr", ElementFromPoint, "int", UIA_TextPattern2Id := 10024, "ptr*", IUIAutomationTextPattern2)   ; IUIAutomationElement::GetCurrentPattern
DllCall(NumGet(NumGet(IUIAutomationTextPattern2+0)+7*A_PtrSize), "ptr", IUIAutomationTextPattern2, "ptr*", IUIAutomationTextRange_DocumentRange)   ; IUIAutomationTextPattern::get_DocumentRange
DllCall(NumGet(NumGet(IUIAutomationTextPattern2+0)+10*A_PtrSize), "ptr", IUIAutomationTextPattern2, "int*", isActive, "ptr*", IUIAutomationTextRange_CaretRange)   ; IUIAutomationTextPattern2::GetCaretRange
DllCall(NumGet(NumGet(IUIAutomationTextRange_CaretRange+0)+5*A_PtrSize), "ptr", IUIAutomationTextRange_CaretRange, "int", TextPatternRangeEndpoint_Start := 0, "ptr", IUIAutomationTextRange_DocumentRange, "int", TextPatternRangeEndpoint_Start := 0, "uint*", caretPos)   ; IUIAutomationTextRange::CompareEndpoints
msgbox % caretPos

Descolada
Posts: 1123
Joined: 23 Dec 2021, 02:30

Re: Acc v2

Post by Descolada » 11 Sep 2022, 07:57

@malcev, yeah but unfortunately it requires the app to implement TextPattern2 which is rather rare. For example in Chrome it never works...

neogna2
Posts: 590
Joined: 15 Sep 2016, 15:44

Re: Acc v2

Post by neogna2 » 07 Dec 2022, 18:42

@Descolada I've used your Acc v2 more by now and it is just great. There's a bit of a hurdle to get good at the FindFirst syntax but the upside is more reliable code compared to passing in hardcoded paths with previous Acc libraries.

One issue I have is with Location as condition in FindFirst (and thus in ValidateCondition). Calling FindFirst({Location: Loc}) where Loc is a location object previously returned from Acc I get Error: Value not enumerable. at this line
https://github.com/Descolada/Acc-v2/blob/main/Lib/Acc.ahk#L821

I think on that line cond is an object so we need OwnProps() to enumerate and then on the next line change the left side comparator to get a property of Location.
So from

Code: Select all

if (prop = "Location") {
    for lprop, lval in cond {
        if this.%lprop% != lval
            return 0
    }
to the following seems to fix the issue

Code: Select all

if (prop = "Location") {
    for lprop, lval in cond.OwnProps() {
        if this.Location.%lprop% != lval
            return 0
    }

edit:
I also in some windows get another error (even after the above fix) when using FindFirst with a Location condition, for example when the element is a filename in File Explorer
Error: (0x80004005) Unspecified error (null) Source: (null) Specifically: accLocation at this line
https://github.com/Descolada/Acc-v2/blob/main/Lib/Acc.ahk#L565

To reproduce open a File Explorer window and run this

Code: Select all

WinActivate("ahk_class CabinetWClass")
oAcc := Acc.ObjectFromWindow( WinExist("A") )
oItem := oAcc.FindFirst({Role: 34})
MsgBox oItem.Name ;the name of the first file in Explorer
oItem2 := oAcc.FindFirst({Location: oItem.Location}) ;ERROR
MsgBox oItem2.Name
I think the issue is that as FindFirst traverses elements and validates the conditions it encounters some element with no Location data so the accLocation call throws. The Acc Viewer doesn't have the same problem because it uses Try at L1201

We can avoid the error if we change at L565

Code: Select all

x:=Buffer(4), y:=Buffer(4), w:=Buffer(4), h:=Buffer(4)
this.oAcc.accLocation(ComValue(0x4003, x.ptr, 1), ComValue(0x4003, y.ptr, 1), ComValue(0x4003, w.ptr, 1), ComValue(0x4003, h.ptr, 1), this.childId)
Return {x:NumGet(x,0,"int"), y:NumGet(y,0,"int"), w:NumGet(w,0,"int"), h:NumGet(h,0,"int")}
to use Try and to zerofill the buffers

Code: Select all

x:=Buffer(4, 0), y:=Buffer(4, 0), w:=Buffer(4, 0), h:=Buffer(4, 0) ;zerofill to have NumGet return 0 on Try fail
Try this.oAcc.accLocation(ComValue(0x4003, x.ptr, 1), ComValue(0x4003, y.ptr, 1), ComValue(0x4003, w.ptr, 1), ComValue(0x4003, h.ptr, 1), this.childId)
Return {x:NumGet(x,0,"int"), y:NumGet(y,0,"int"), w:NumGet(w,0,"int"), h:NumGet(h,0,"int")}
(Alternatively use Try and let Catch return an object with x y w h value 0)

Maybe other FindFirst condition cases also need to use Try? I'll test this more and report back.

Descolada
Posts: 1123
Joined: 23 Dec 2021, 02:30

Re: Acc v2

Post by Descolada » 08 Dec 2022, 11:48

@neogna2, thanks for the bug report, that should be fixed now.

I had overlooked that accLocation property may be missing for IAccessible objects. In this library if a property is missing then an error is thrown - this way you can differentiate between having an empty value ("", 0) or a null value (unset). I've now added a way of matching for unset properties using an empty object to achieve that, and ValidateCondition will silently handle differentiating between them. This means your code snippet works now, and also oAcc.FindFirst({Location: {}}) will return an element that will throw when Location property is tried to access (oAcc.FindFirst({Location: {}}).Location will throw). This will be a different element than oAcc.FindFirst({Location: {x:0,y:0,w:0,h:0}}).

Let me know if you find anything else ;)

neogna2
Posts: 590
Joined: 15 Sep 2016, 15:44

Re: Acc v2

Post by neogna2 » 09 Dec 2022, 06:14

Thanks, that fixed the issues. Nice enhancements too!
Descolada wrote:
08 Dec 2022, 11:48
In this library if a property is missing then an error is thrown
Only to clarify that for myself and others, no error is (after the latest fixes) thrown inside FindFirst searches. It is only after an object is found and we access a non-existant property that an error is to be expected.

A built in method to get an object's path could be useful, for example after ObjectFromPoint(), without an extra FindFirst step.
I use the below helper function which is pretty fast, but maybe you know a faster or simpler way?

Code: Select all

;Get Acc object's path. Default: object at mouse pointer. (Helper function for v2 Acc library)
AccGetPath(oA := Acc.ObjectFromPoint() ) {
    oWin := Acc.ObjectFromWindow("ahk_id " oA.WinID)
    While !oA.IsEqual(oWin)
        for i, child in oP := oA.Parent
            if child.IsEqual(oA)
            {
                Path := IsSet(Path) ? i "," Path : i
                oA := oP
                break
            }
    return Path
}

Post Reply

Return to “Scripts and Functions (v2)”