Mercurial > minori
comparison dep/animone/src/a11y/win32.cc @ 340:74e2365326c6
dep/animone: add experimental accessibility strategy
I also moved most of the functions out of util/win32.cc, because that
file is meant for things that are shared between the different functions,
and currently that is only wide string conversion helpers.
| author | Paper <paper@paper.us.eu.org> |
|---|---|
| date | Wed, 19 Jun 2024 23:13:55 -0400 |
| parents | |
| children | 052ec053ee37 |
comparison
equal
deleted
inserted
replaced
| 339:eac06513db86 | 340:74e2365326c6 |
|---|---|
| 1 #include <functional> | |
| 2 #include <string> | |
| 3 #include <vector> | |
| 4 | |
| 5 #include <windows.h> | |
| 6 #include <uiautomation.h> | |
| 7 | |
| 8 #include "animone/a11y.h" | |
| 9 #include "animone/a11y/win32.h" | |
| 10 | |
| 11 namespace animone::internal::win32 { | |
| 12 | |
| 13 // Windows Accessibility API reference: | |
| 14 // https://msdn.microsoft.com/en-us/library/windows/desktop/ff486375.aspx | |
| 15 | |
| 16 // Commonly used interfaces | |
| 17 using Element = IUIAutomationElement; | |
| 18 using TreeWalker = IUIAutomationTreeWalker; | |
| 19 using ValuePattern = IUIAutomationValuePattern; | |
| 20 | |
| 21 using element_proc_t = std::function<TreeScope(Element&)>; | |
| 22 using properties_t = std::vector<std::pair<long, bool>>; | |
| 23 | |
| 24 // The main interface that is used throughout this file. Must be initialized | |
| 25 // before it can be used for the first time. | |
| 26 static ComInterface<IUIAutomation> ui_automation; | |
| 27 | |
| 28 //////////////////////////////////////////////////////////////////////////////// | |
| 29 | |
| 30 static bool InitializeUIAutomation() { | |
| 31 if (ui_automation) | |
| 32 return true; | |
| 33 | |
| 34 // COM library must be initialized on the current thread before calling | |
| 35 // CoCreateInstance. | |
| 36 ::CoInitialize(nullptr); | |
| 37 | |
| 38 IUIAutomation* ui_automation_interface = nullptr; | |
| 39 const auto result = ::CoCreateInstance( | |
| 40 CLSID_CUIAutomation, nullptr, CLSCTX_INPROC_SERVER, IID_IUIAutomation, | |
| 41 reinterpret_cast<void**>(&ui_automation_interface)); | |
| 42 ui_automation.reset(ui_automation_interface); | |
| 43 | |
| 44 return SUCCEEDED(result); | |
| 45 } | |
| 46 | |
| 47 //////////////////////////////////////////////////////////////////////////////// | |
| 48 | |
| 49 static Element* GetElementFromHandle(HWND hwnd) { | |
| 50 Element* element = nullptr; | |
| 51 ui_automation->ElementFromHandle(static_cast<UIA_HWND>(hwnd), &element); | |
| 52 return element; | |
| 53 } | |
| 54 | |
| 55 static std::wstring GetElementName(Element& element) { | |
| 56 std::wstring element_name; | |
| 57 | |
| 58 BSTR bstr = nullptr; | |
| 59 if (SUCCEEDED(element.get_CurrentName(&bstr)) && bstr) { | |
| 60 element_name = bstr; | |
| 61 ::SysFreeString(bstr); | |
| 62 } | |
| 63 | |
| 64 return element_name; | |
| 65 } | |
| 66 | |
| 67 static std::wstring GetElementValue(Element& element) { | |
| 68 std::wstring element_value; | |
| 69 | |
| 70 ValuePattern* value_pattern_interface = nullptr; | |
| 71 element.GetCurrentPatternAs( | |
| 72 UIA_ValuePatternId, IID_PPV_ARGS(&value_pattern_interface)); | |
| 73 ComInterface<ValuePattern> value_pattern(value_pattern_interface); | |
| 74 | |
| 75 if (value_pattern) { | |
| 76 BSTR bstr = nullptr; | |
| 77 if (SUCCEEDED(value_pattern->get_CurrentValue(&bstr)) && bstr) { | |
| 78 element_value = bstr; | |
| 79 ::SysFreeString(bstr); | |
| 80 } | |
| 81 } | |
| 82 | |
| 83 return element_value; | |
| 84 } | |
| 85 | |
| 86 //////////////////////////////////////////////////////////////////////////////// | |
| 87 | |
| 88 static bool VerifyElementProperties(Element& element, const properties_t& properties) { | |
| 89 VARIANT v = {}; | |
| 90 for (const auto& pair : properties) { | |
| 91 if (FAILED(element.GetCurrentPropertyValue(pair.first, &v))) | |
| 92 return false; | |
| 93 if (v.boolVal != (pair.second ? VARIANT_TRUE : VARIANT_FALSE)) | |
| 94 return false; | |
| 95 } | |
| 96 | |
| 97 return true; | |
| 98 } | |
| 99 | |
| 100 static bool IsAddressBarElement(Element& element) { | |
| 101 static const properties_t properties = { | |
| 102 {UIA_IsEnabledPropertyId, true}, | |
| 103 {UIA_IsKeyboardFocusablePropertyId, true}, | |
| 104 {UIA_IsValuePatternAvailablePropertyId, true}, | |
| 105 {UIA_ValueIsReadOnlyPropertyId, false}, | |
| 106 }; | |
| 107 | |
| 108 return VerifyElementProperties(element, properties); | |
| 109 } | |
| 110 | |
| 111 static bool IsTabsElement(Element& element) { | |
| 112 static const properties_t properties = { | |
| 113 {UIA_ValueIsReadOnlyPropertyId, true}, | |
| 114 }; | |
| 115 | |
| 116 return VerifyElementProperties(element, properties); | |
| 117 } | |
| 118 | |
| 119 //////////////////////////////////////////////////////////////////////////////// | |
| 120 | |
| 121 static void WalkElements(TreeWalker& tree_walker, Element& parent, TreeScope scope, | |
| 122 size_t depth, element_proc_t element_proc) { | |
| 123 constexpr size_t kMaxTreeDepth = 16; // arbitrary value | |
| 124 if (depth > kMaxTreeDepth) | |
| 125 return; | |
| 126 | |
| 127 if (scope & TreeScope_Element) | |
| 128 scope = element_proc(parent); | |
| 129 | |
| 130 auto descend = [](TreeScope scope) { | |
| 131 return (scope & TreeScope_Children) || (scope & TreeScope_Descendants); | |
| 132 }; | |
| 133 | |
| 134 if (descend(scope)) { | |
| 135 Element* first_element = nullptr; | |
| 136 tree_walker.GetFirstChildElement(&parent, &first_element); | |
| 137 ComInterface<Element> element(first_element); | |
| 138 | |
| 139 while (element) { | |
| 140 scope = element_proc(*element); | |
| 141 | |
| 142 if (descend(scope)) | |
| 143 WalkElements(tree_walker, *element, scope, depth + 1, element_proc); | |
| 144 | |
| 145 Element* next_element = nullptr; | |
| 146 tree_walker.GetNextSiblingElement(element.get(), &next_element); | |
| 147 element.reset(next_element); | |
| 148 } | |
| 149 } | |
| 150 } | |
| 151 | |
| 152 static bool FindWebBrowserElements(Element& parent, std::wstring& address, | |
| 153 std::vector<std::wstring>& tabs) { | |
| 154 TreeWalker* tree_walker_interface = nullptr; | |
| 155 ui_automation->get_ControlViewWalker(&tree_walker_interface); | |
| 156 ComInterface<TreeWalker> tree_walker(tree_walker_interface); | |
| 157 | |
| 158 if (!tree_walker) | |
| 159 return false; | |
| 160 | |
| 161 auto element_proc = [&](Element& element) -> TreeScope { | |
| 162 CONTROLTYPEID control_type_id = 0; | |
| 163 element.get_CurrentControlType(&control_type_id); | |
| 164 | |
| 165 switch (control_type_id) { | |
| 166 default: | |
| 167 // Are we done? | |
| 168 if (!address.empty() && !tabs.empty()) | |
| 169 return TreeScope_Element; | |
| 170 // Otherwise continue descending the tree. | |
| 171 return TreeScope_Descendants; | |
| 172 | |
| 173 case UIA_DocumentControlTypeId: | |
| 174 case UIA_MenuBarControlTypeId: | |
| 175 case UIA_TitleBarControlTypeId: | |
| 176 // We do not need to walk through these nodes. In fact, skipping | |
| 177 // documents dramatically improves our performance on worst case | |
| 178 // scenarios. This is the whole reason we are walking the tree rather | |
| 179 // than using FindFirst and FindAll methods. | |
| 180 return TreeScope_Element; | |
| 181 | |
| 182 case UIA_EditControlTypeId: | |
| 183 // Here we assume that the first edit control that fits our properties | |
| 184 // is the address bar (e.g. "Omnibox" on Chrome, "Awesome Bar" on | |
| 185 // Firefox). This element is named differently on each web browser | |
| 186 // (e.g. "Address and search bar" on Chrome, "Search or enter address" | |
| 187 // on Firefox). This name can change depending on the browser | |
| 188 // language. However, we are only interested in the element value, | |
| 189 // which usually gives us the URL of the current page. | |
| 190 if (address.empty() && IsAddressBarElement(element)) { | |
| 191 address = GetElementValue(element); | |
| 192 return TreeScope_Element; | |
| 193 } else { | |
| 194 // Opera has an edit control ("Address field") within another edit | |
| 195 // control ("Address bar"). | |
| 196 return TreeScope_Descendants; | |
| 197 } | |
| 198 | |
| 199 case UIA_TabControlTypeId: | |
| 200 if (tabs.empty() && IsTabsElement(element)) | |
| 201 return TreeScope_Children; | |
| 202 return TreeScope_Element; | |
| 203 | |
| 204 case UIA_TabItemControlTypeId: | |
| 205 tabs.push_back(GetElementName(element)); | |
| 206 return TreeScope_Element; | |
| 207 } | |
| 208 }; | |
| 209 | |
| 210 WalkElements(*tree_walker, parent, TreeScope_Subtree, 0, element_proc); | |
| 211 return true; | |
| 212 } | |
| 213 | |
| 214 /* ------------------------------------------------------------------------------------ */ | |
| 215 | |
| 216 bool GetWebBrowserInformation(const Window& window, web_browser_proc_t web_browser_proc) { | |
| 217 if (!web_browser_proc) | |
| 218 return false; | |
| 219 | |
| 220 if (!InitializeUIAutomation()) | |
| 221 return false; | |
| 222 | |
| 223 ComInterface<Element> parent(GetElementFromHandle(hwnd)); | |
| 224 if (!parent) | |
| 225 return false; | |
| 226 | |
| 227 const std::string title = ToUtf8String(GetElementName(*parent)); | |
| 228 web_browser_proc({WebBrowserInformationType::Title, title}); | |
| 229 | |
| 230 std::wstring address; | |
| 231 std::vector<std::wstring> tabs; | |
| 232 | |
| 233 if (!FindWebBrowserElements(*parent, address, tabs)) | |
| 234 return false; | |
| 235 | |
| 236 web_browser_proc({WebBrowserInformationType::Address, ToUtf8String(address)}); | |
| 237 for (const auto& tab : tabs) | |
| 238 web_browser_proc({WebBrowserInformationType::Tab, ToUtf8String(tab)}); | |
| 239 | |
| 240 return true; | |
| 241 } | |
| 242 | |
| 243 } // namespace animone::internal::win32 |
