-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Windows IA2 support #10
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,130 @@ | ||||||||
from ..executors.ia2.constants import * | ||||||||
|
||||||||
import json | ||||||||
import sys | ||||||||
import time | ||||||||
|
||||||||
import ctypes | ||||||||
from ctypes import POINTER, byref | ||||||||
from ctypes.wintypes import BOOL, HWND, LPARAM, POINT | ||||||||
|
||||||||
import comtypes.client | ||||||||
from comtypes import COMError, IServiceProvider | ||||||||
|
||||||||
CHILDID_SELF = 0 | ||||||||
OBJID_CLIENT = -4 | ||||||||
|
||||||||
user32 = ctypes.windll.user32 | ||||||||
oleacc = ctypes.oledll.oleacc | ||||||||
oleaccMod = comtypes.client.GetModule("oleacc.dll") | ||||||||
IAccessible = oleaccMod.IAccessible | ||||||||
del oleaccMod | ||||||||
spectranaut marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
|
||||||||
## CoCreateInstance of UIA also initialized IA2 | ||||||||
alice marked this conversation as resolved.
Show resolved
Hide resolved
spectranaut marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
uiaMod = comtypes.client.GetModule("UIAutomationCore.dll") | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So we're not using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that is right. I think because of the IA2->UIA bridge that windows currently ships with. |
||||||||
uiaClient = comtypes.CoCreateInstance( | ||||||||
uiaMod.CUIAutomation._reg_clsid_, | ||||||||
interface=uiaMod.IUIAutomation, | ||||||||
clsctx=comtypes.CLSCTX_INPROC_SERVER, | ||||||||
) | ||||||||
|
||||||||
def accessible_object_from_window(hwnd, objectID=OBJID_CLIENT): | ||||||||
spectranaut marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
p = POINTER(IAccessible)() | ||||||||
oleacc.AccessibleObjectFromWindow( | ||||||||
hwnd, objectID, byref(IAccessible._iid_), byref(p) | ||||||||
) | ||||||||
return p | ||||||||
|
||||||||
def name_from_hwnd(hwnd): | ||||||||
MAX_CHARS = 257 | ||||||||
buffer = ctypes.create_unicode_buffer(MAX_CHARS) | ||||||||
user32.GetWindowTextW(hwnd, buffer, MAX_CHARS) | ||||||||
return buffer.value | ||||||||
|
||||||||
def get_browser_hwnd(product_name): | ||||||||
found = [] | ||||||||
|
||||||||
@ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does this do? |
||||||||
def callback(hwnd, lParam): | ||||||||
spectranaut marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
name = name_from_hwnd(hwnd) | ||||||||
if product_name not in name.lower(): | ||||||||
spectranaut marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
return True | ||||||||
spectranaut marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
found.append(hwnd) | ||||||||
return False | ||||||||
spectranaut marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
|
||||||||
user32.EnumWindows(callback, LPARAM(0)) | ||||||||
spectranaut marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
if not found: | ||||||||
raise LookupError(f"Couldn't find {product_name} HWND") | ||||||||
return found[0] | ||||||||
|
||||||||
def to_ia2(node): | ||||||||
serv = node.QueryInterface(IServiceProvider) | ||||||||
return serv.QueryService(IAccessible2._iid_, IAccessible2) | ||||||||
spectranaut marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
|
||||||||
def find_browser(product_name): | ||||||||
hwnd = get_browser_hwnd(product_name) | ||||||||
root = accessible_object_from_window(hwnd) | ||||||||
return to_ia2(root) | ||||||||
|
||||||||
def poll_for_active_tab(title, root): | ||||||||
active_tab = find_active_tab(title, root) | ||||||||
iterations = 0 | ||||||||
while not active_tab: | ||||||||
time.sleep(0.01) | ||||||||
active_tab = find_active_tab(title, root) | ||||||||
iterations += 1 | ||||||||
|
||||||||
print(f"found active tab in {iterations} iterations", file=sys.stderr) | ||||||||
spectranaut marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
return active_tab | ||||||||
|
||||||||
def find_active_tab(title, root): | ||||||||
spectranaut marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
for i in range(1, root.accChildCount + 1): | ||||||||
child = to_ia2(root.accChild(i)) | ||||||||
if child.accRole(CHILDID_SELF) == ROLE_SYSTEM_DOCUMENT: | ||||||||
if child.accName(CHILDID_SELF) == title: | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
(Not an actual suggestion to be applied directly, just documenting how we get the URL, if we wanted to do that) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done! |
||||||||
return child | ||||||||
# No need to search within documents. | ||||||||
return | ||||||||
descendant = find_active_tab(title, child) | ||||||||
if descendant: | ||||||||
return descendant | ||||||||
|
||||||||
def find_ia2_node(root, id): | ||||||||
search = f"id:{id};" | ||||||||
for i in range(1, root.accChildCount + 1): | ||||||||
child = to_ia2(root.accChild(i)) | ||||||||
if child.attributes and search in child.attributes: | ||||||||
return child | ||||||||
spectranaut marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
descendant = find_ia2_node(child, id) | ||||||||
if descendant: | ||||||||
return descendant | ||||||||
|
||||||||
def serialize_node(node): | ||||||||
node_dictionary = {} | ||||||||
node_dictionary["API"] = "windows" | ||||||||
|
||||||||
# MSAA properties | ||||||||
node_dictionary["name"] = node.accName(CHILDID_SELF) | ||||||||
node_dictionary["msaa_role"] = role_to_string[node.accRole(CHILDID_SELF)] | ||||||||
node_dictionary["msaa_states"] = get_msaa_state_list(node.accState(CHILDID_SELF)) | ||||||||
|
||||||||
# IAccessible2 properties | ||||||||
node_dictionary["ia2_role"] = role_to_string[node.role()] | ||||||||
node_dictionary["ia2_states"] = get_state_list(node.states) | ||||||||
|
||||||||
return node_dictionary | ||||||||
|
||||||||
class WindowsAccessibilityExecutorImpl: | ||||||||
def setup(self, product_name): | ||||||||
self.product_name = product_name | ||||||||
|
||||||||
def get_accessibility_api_node(self, title, dom_id): | ||||||||
self.root = find_browser(self.product_name) | ||||||||
if not self.root: | ||||||||
raise Exception(f"Couldn't find browser {self.product_name}.") | ||||||||
|
||||||||
active_tab = poll_for_active_tab(title, self.root) | ||||||||
node = find_ia2_node(active_tab, dom_id) | ||||||||
if not node: | ||||||||
raise Exception(f"Couldn't find node with ID {dom_id}.") | ||||||||
return json.dumps(serialize_node(node)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we sent
location.href
here instead ofdocument.title
, we could match based on the URL rather than the (potentially ambiguous) title.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good idea!