// Copyright 2017 The Chromium Embedded Framework Authors. Portions copyright // 2013 The Chromium Authors. All rights reserved. Use of this source code is // governed by a BSD-style license that can be found in the LICENSE file. // This class implements our accessible proxy object that handles moving // data back and forth between MSAA clients and CefClient renderers. // Sample implementation based on ui\accessibility\ax_platform_node_win.h #include "tests/cefclient/browser/osr_accessibility_node.h" #if defined(CEF_USE_ATL) #include #include #include #include "tests/cefclient/browser/osr_accessibility_helper.h" namespace client { // Return CO_E_OBJNOTCONNECTED for accessible objects thar still exists but the // window and/or object it references has been destroyed. #define DATACHECK(node) (node) ? S_OK : CO_E_OBJNOTCONNECTED #define VALID_CHILDID(varChild) ((varChild.vt == VT_I4)) namespace { // Helper function to convert a rectangle from client coordinates to screen // coordinates. void ClientToScreen(HWND hwnd, LPRECT lpRect) { if (lpRect) { POINT ptTL = {lpRect->left, lpRect->top}; POINT ptBR = {lpRect->right, lpRect->bottom}; // Win32 API only provides the call for a point. ClientToScreen(hwnd, &ptTL); ClientToScreen(hwnd, &ptBR); SetRect(lpRect, ptTL.x, ptTL.y, ptBR.x, ptBR.y); } } // Helper function to convert to MSAARole int AxRoleToMSAARole(const std::string& role_string) { if (role_string == "alert") return ROLE_SYSTEM_ALERT; if (role_string == "application") return ROLE_SYSTEM_APPLICATION; if (role_string == "buttonDropDown") return ROLE_SYSTEM_BUTTONDROPDOWN; if (role_string == "popUpButton") return ROLE_SYSTEM_BUTTONMENU; if (role_string == "checkBox") return ROLE_SYSTEM_CHECKBUTTON; if (role_string == "comboBox") return ROLE_SYSTEM_COMBOBOX; if (role_string == "dialog") return ROLE_SYSTEM_DIALOG; if (role_string == "genericContainer") return ROLE_SYSTEM_GROUPING; if (role_string == "group") return ROLE_SYSTEM_GROUPING; if (role_string == "image") return ROLE_SYSTEM_GRAPHIC; if (role_string == "link") return ROLE_SYSTEM_LINK; if (role_string == "locationBar") return ROLE_SYSTEM_GROUPING; if (role_string == "menuBar") return ROLE_SYSTEM_MENUBAR; if (role_string == "menuItem") return ROLE_SYSTEM_MENUITEM; if (role_string == "menuListPopup") return ROLE_SYSTEM_MENUPOPUP; if (role_string == "tree") return ROLE_SYSTEM_OUTLINE; if (role_string == "treeItem") return ROLE_SYSTEM_OUTLINEITEM; if (role_string == "tab") return ROLE_SYSTEM_PAGETAB; if (role_string == "tabList") return ROLE_SYSTEM_PAGETABLIST; if (role_string == "pane") return ROLE_SYSTEM_PANE; if (role_string == "progressIndicator") return ROLE_SYSTEM_PROGRESSBAR; if (role_string == "button") return ROLE_SYSTEM_PUSHBUTTON; if (role_string == "radioButton") return ROLE_SYSTEM_RADIOBUTTON; if (role_string == "scrollBar") return ROLE_SYSTEM_SCROLLBAR; if (role_string == "splitter") return ROLE_SYSTEM_SEPARATOR; if (role_string == "slider") return ROLE_SYSTEM_SLIDER; if (role_string == "staticText") return ROLE_SYSTEM_STATICTEXT; if (role_string == "textField") return ROLE_SYSTEM_TEXT; if (role_string == "titleBar") return ROLE_SYSTEM_TITLEBAR; if (role_string == "toolbar") return ROLE_SYSTEM_TOOLBAR; if (role_string == "webView") return ROLE_SYSTEM_GROUPING; if (role_string == "window") return ROLE_SYSTEM_WINDOW; if (role_string == "client") return ROLE_SYSTEM_CLIENT; // This is the default role for MSAA. return ROLE_SYSTEM_CLIENT; } static inline int MiddleX(const CefRect& rect) { return rect.x + rect.width / 2; } static inline int MiddleY(const CefRect& rect) { return rect.y + rect.height / 2; } } // namespace struct CefIAccessible : public IAccessible { public: // Implement IUnknown STDMETHODIMP QueryInterface(REFIID riid, void** ppvObject); STDMETHODIMP_(ULONG) AddRef(); STDMETHODIMP_(ULONG) Release(); // // IAccessible methods. // // Retrieves the child element or child object at a given point on the screen. STDMETHODIMP accHitTest(LONG x_left, LONG y_top, VARIANT* child) override; // Performs the object's default action. STDMETHODIMP accDoDefaultAction(VARIANT var_id) override; // Retrieves the specified object's current screen location. STDMETHODIMP accLocation(LONG* x_left, LONG* y_top, LONG* width, LONG* height, VARIANT var_id) override; // Traverses to another UI element and retrieves the object. STDMETHODIMP accNavigate(LONG nav_dir, VARIANT start, VARIANT* end) override; // Retrieves an IDispatch interface pointer for the specified child. STDMETHODIMP get_accChild(VARIANT var_child, IDispatch** disp_child) override; // Retrieves the number of accessible children. STDMETHODIMP get_accChildCount(LONG* child_count) override; // Retrieves a string that describes the object's default action. STDMETHODIMP get_accDefaultAction(VARIANT var_id, BSTR* default_action) override; // Retrieves the tooltip description. STDMETHODIMP get_accDescription(VARIANT var_id, BSTR* desc) override; // Retrieves the object that has the keyboard focus. STDMETHODIMP get_accFocus(VARIANT* focus_child) override; // Retrieves the specified object's shortcut. STDMETHODIMP get_accKeyboardShortcut(VARIANT var_id, BSTR* access_key) override; // Retrieves the name of the specified object. STDMETHODIMP get_accName(VARIANT var_id, BSTR* name) override; // Retrieves the IDispatch interface of the object's parent. STDMETHODIMP get_accParent(IDispatch** disp_parent) override; // Retrieves information describing the role of the specified object. STDMETHODIMP get_accRole(VARIANT var_id, VARIANT* role) override; // Retrieves the current state of the specified object. STDMETHODIMP get_accState(VARIANT var_id, VARIANT* state) override; // Gets the help string for the specified object. STDMETHODIMP get_accHelp(VARIANT var_id, BSTR* help) override; // Retrieve or set the string value associated with the specified object. // Setting the value is not typically used by screen readers, but it's // used frequently by automation software. STDMETHODIMP get_accValue(VARIANT var_id, BSTR* value) override; STDMETHODIMP put_accValue(VARIANT var_id, BSTR new_value) override; // IAccessible methods not implemented. STDMETHODIMP get_accSelection(VARIANT* selected) override; STDMETHODIMP accSelect(LONG flags_sel, VARIANT var_id) override; STDMETHODIMP get_accHelpTopic(BSTR* help_file, VARIANT var_id, LONG* topic_id) override; STDMETHODIMP put_accName(VARIANT var_id, BSTR put_name) override; // Implement IDispatch STDMETHODIMP GetTypeInfoCount(unsigned int FAR* pctinfo); STDMETHODIMP GetTypeInfo(unsigned int iTInfo, LCID lcid, ITypeInfo FAR* FAR* ppTInfo); STDMETHODIMP GetIDsOfNames(REFIID riid, OLECHAR FAR* FAR* rgszNames, unsigned int cNames, LCID lcid, DISPID FAR* rgDispId); STDMETHODIMP Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS FAR* pDispParams, VARIANT FAR* pVarResult, EXCEPINFO FAR* pExcepInfo, unsigned int FAR* puArgErr); CefIAccessible(OsrAXNode* node) : node_(node), ref_count_(0) {} // Remove the node reference when OsrAXNode is destroyed, so that // MSAA clients get CO_E_OBJNOTCONNECTED void MarkDestroyed() { node_ = NULL; } protected: // Ref Count ULONG ref_count_; // OsrAXNode* proxy object OsrAXNode* node_; }; // Implement IUnknown // ********************* // Handles ref counting and querying for other supported interfaces. // We only support, IUnknown, IDispatch and IAccessible. STDMETHODIMP CefIAccessible::QueryInterface(REFIID riid, void** ppvObject) { if (riid == IID_IAccessible) *ppvObject = static_cast(this); else if (riid == IID_IDispatch) *ppvObject = static_cast(this); else if (riid == IID_IUnknown) *ppvObject = static_cast(this); else *ppvObject = NULL; if (*ppvObject) reinterpret_cast(*ppvObject)->AddRef(); return (*ppvObject) ? S_OK : E_NOINTERFACE; } // Increments COM objects refcount required by IUnknown for reference counting STDMETHODIMP_(ULONG) CefIAccessible::AddRef() { return InterlockedIncrement((LONG volatile*)&ref_count_); } STDMETHODIMP_(ULONG) CefIAccessible::Release() { ULONG ulRefCnt = InterlockedDecrement((LONG volatile*)&ref_count_); if (ulRefCnt == 0) { // Remove reference from OsrAXNode if (node_) node_->Destroy(); delete this; } return ulRefCnt; } // Implement IAccessible // ********************* // Returns the parent IAccessible in the form of an IDispatch interface. STDMETHODIMP CefIAccessible::get_accParent(IDispatch** ppdispParent) { HRESULT retCode = DATACHECK(node_); if (SUCCEEDED(retCode)) { if (ppdispParent) { CefNativeAccessible* parent = node_->GetParentAccessibleObject(); if (!parent) { // Find our parent window HWND hWnd = ::GetParent(node_->GetWindowHandle()); // if we have a window attempt to get its IAccessible pointer if (hWnd) { AccessibleObjectFromWindow(hWnd, (DWORD)OBJID_CLIENT, IID_IAccessible, (void**)(&parent)); } } if (parent) parent->AddRef(); *ppdispParent = parent; retCode = (*ppdispParent) ? S_OK : S_FALSE; } } else { retCode = E_INVALIDARG; } return retCode; } // Returns the number of children we have for this element. STDMETHODIMP CefIAccessible::get_accChildCount(long* pcountChildren) { HRESULT retCode = DATACHECK(node_); if (SUCCEEDED(retCode) && pcountChildren) { // Get Child node count for this from Accessibility tree *pcountChildren = node_->GetChildCount(); } else { retCode = E_INVALIDARG; } return retCode; } // Returns a child IAccessible object. STDMETHODIMP CefIAccessible::get_accChild(VARIANT varChild, IDispatch** ppdispChild) { HRESULT retCode = DATACHECK(node_); if (SUCCEEDED(retCode)) { int numChilds = node_->GetChildCount(); // Mark Leaf node if there are no child if (numChilds <= 0) { *ppdispChild = NULL; return S_FALSE; } else { if (ppdispChild && VALID_CHILDID(varChild)) { if (varChild.lVal == CHILDID_SELF) { *ppdispChild = this; } else { // Convert to 0 based index and get Child Node. OsrAXNode* child = node_->ChildAtIndex(varChild.lVal - 1); // Fallback to focused node if (!child) child = node_->GetAccessibilityHelper()->GetFocusedNode(); *ppdispChild = child->GetNativeAccessibleObject(node_); } if (*ppdispChild == NULL) retCode = S_FALSE; else (*ppdispChild)->AddRef(); } } } return retCode; } // Check and returns the accessible name for element from accessibility tree STDMETHODIMP CefIAccessible::get_accName(VARIANT varChild, BSTR* pszName) { HRESULT retCode = DATACHECK(node_); if (SUCCEEDED(retCode)) { if (pszName && VALID_CHILDID(varChild)) { std::string name = node_->AxName(); CComBSTR bstrResult(name.c_str()); *pszName = bstrResult.Detach(); } } else { retCode = E_INVALIDARG; } return retCode; } // Check and returns the value for element from accessibility tree STDMETHODIMP CefIAccessible::get_accValue(VARIANT varChild, BSTR* pszValue) { HRESULT retCode = DATACHECK(node_); if (SUCCEEDED(retCode)) { if (pszValue && VALID_CHILDID(varChild)) { std::string name = node_->AxValue(); CComBSTR bstrResult(name.c_str()); *pszValue = bstrResult.Detach(); } } else { retCode = E_INVALIDARG; } return retCode; } // Check and returns the description for element from accessibility tree STDMETHODIMP CefIAccessible::get_accDescription(VARIANT varChild, BSTR* pszDescription) { HRESULT retCode = DATACHECK(node_); if (SUCCEEDED(retCode)) { if (pszDescription && VALID_CHILDID(varChild)) { std::string name = node_->AxDescription(); CComBSTR bstrResult(name.c_str()); *pszDescription = bstrResult.Detach(); } } else { retCode = E_INVALIDARG; } return retCode; } // Check and returns the MSAA Role for element from accessibility tree STDMETHODIMP CefIAccessible::get_accRole(VARIANT varChild, VARIANT* pvarRole) { HRESULT retCode = DATACHECK(node_); if (SUCCEEDED(retCode)) { // Get the accessibilty role and Map to MSAA Role if (pvarRole) { pvarRole->vt = VT_I4; pvarRole->lVal = AxRoleToMSAARole(node_->AxRole()); } else { retCode = E_INVALIDARG; } } return retCode; } // Check and returns Accessibility State for element from accessibility tree STDMETHODIMP CefIAccessible::get_accState(VARIANT varChild, VARIANT* pvarState) { HRESULT retCode = DATACHECK(node_); if (SUCCEEDED(retCode)) { if (pvarState) { pvarState->vt = VT_I4; pvarState->lVal = (GetFocus() == node_->GetWindowHandle()) ? STATE_SYSTEM_FOCUSED : 0; pvarState->lVal |= STATE_SYSTEM_PRESSED; pvarState->lVal |= STATE_SYSTEM_FOCUSABLE; // For child if (varChild.lVal == CHILDID_SELF) { DWORD dwStyle = GetWindowLong(node_->GetWindowHandle(), GWL_STYLE); pvarState->lVal |= ((dwStyle & WS_VISIBLE) == 0) ? STATE_SYSTEM_INVISIBLE : 0; pvarState->lVal |= ((dwStyle & WS_DISABLED) > 0) ? STATE_SYSTEM_UNAVAILABLE : 0; } } else { retCode = E_INVALIDARG; } } return retCode; } // Check and returns Accessibility Shortcut if any for element STDMETHODIMP CefIAccessible::get_accKeyboardShortcut( VARIANT varChild, BSTR* pszKeyboardShortcut) { HRESULT retCode = DATACHECK(node_); if (SUCCEEDED(retCode)) { if (pszKeyboardShortcut && VALID_CHILDID(varChild)) *pszKeyboardShortcut = ::SysAllocString(L"None"); else retCode = E_INVALIDARG; } return retCode; } // Return focused element from the accessibility tree STDMETHODIMP CefIAccessible::get_accFocus(VARIANT* pFocusChild) { HRESULT retCode = DATACHECK(node_); if (SUCCEEDED(retCode)) { OsrAXNode* focusedNode = node_->GetAccessibilityHelper()->GetFocusedNode(); CefNativeAccessible* nativeObj = NULL; if (focusedNode) nativeObj = focusedNode->GetNativeAccessibleObject(NULL); if (nativeObj) { if (nativeObj == this) { pFocusChild->vt = VT_I4; pFocusChild->lVal = CHILDID_SELF; } else { pFocusChild->vt = VT_DISPATCH; pFocusChild->pdispVal = nativeObj; pFocusChild->pdispVal->AddRef(); } } else { pFocusChild->vt = VT_EMPTY; } } return retCode; } // Return a selection list for multiple selection items. STDMETHODIMP CefIAccessible::get_accSelection(VARIANT* pvarChildren) { HRESULT retCode = DATACHECK(node_); if (SUCCEEDED(retCode)) { if (pvarChildren) pvarChildren->vt = VT_EMPTY; else retCode = E_INVALIDARG; } return retCode; } // Return a string description of the default action of our element, eg. push STDMETHODIMP CefIAccessible::get_accDefaultAction(VARIANT varChild, BSTR* pszDefaultAction) { HRESULT retCode = DATACHECK(node_); if (SUCCEEDED(retCode)) { if (pszDefaultAction && VALID_CHILDID(varChild)) *pszDefaultAction = ::SysAllocString(L"Push"); else retCode = E_INVALIDARG; } return retCode; } // child item selectionor for an item to take focus. STDMETHODIMP CefIAccessible::accSelect(long flagsSelect, VARIANT varChild) { HRESULT retCode = DATACHECK(node_); if (SUCCEEDED(retCode)) { if (VALID_CHILDID(varChild)) { HWND hwnd = node_->GetWindowHandle(); // we only support SELFLAG_TAKEFOCUS. if (((flagsSelect & SELFLAG_TAKEFOCUS) > 0) && (GetFocus() == hwnd)) { RECT rcWnd; GetClientRect(hwnd, &rcWnd); InvalidateRect(hwnd, &rcWnd, FALSE); } else { retCode = S_FALSE; } } else { retCode = E_INVALIDARG; } } return retCode; } // Returns back the screen coordinates of our element or one of its childs STDMETHODIMP CefIAccessible::accLocation(long* pxLeft, long* pyTop, long* pcxWidth, long* pcyHeight, VARIANT varChild) { HRESULT retCode = DATACHECK(node_); if (SUCCEEDED(retCode)) { if (pxLeft && pyTop && pcxWidth && pcyHeight && VALID_CHILDID(varChild)) { CefRect loc = node_->AxLocation(); RECT rcItem = {loc.x, loc.y, loc.x + loc.width, loc.y + loc.height}; HWND hwnd = node_->GetWindowHandle(); ClientToScreen(hwnd, &rcItem); *pxLeft = rcItem.left; *pyTop = rcItem.top; *pcxWidth = rcItem.right - rcItem.left; *pcyHeight = rcItem.bottom - rcItem.top; } else { retCode = E_INVALIDARG; } } return retCode; } // Allow clients to move the keyboard focus within the control // Deprecated STDMETHODIMP CefIAccessible::accNavigate(long navDir, VARIANT varStart, VARIANT* pvarEndUpAt) { return E_NOTIMPL; } // Check if the coordinates provided are within our element or child items. STDMETHODIMP CefIAccessible::accHitTest(long xLeft, long yTop, VARIANT* pvarChild) { HRESULT retCode = DATACHECK(node_); if (SUCCEEDED(retCode)) { if (pvarChild) { pvarChild->vt = VT_EMPTY; CefRect loc = node_->AxLocation(); RECT rcItem = {loc.x, loc.y, loc.x + loc.width, loc.y + loc.height}; POINT pt = {xLeft, yTop}; ClientToScreen(node_->GetWindowHandle(), &rcItem); if (PtInRect(&rcItem, pt)) { pvarChild->vt = VT_I4; pvarChild->lVal = 1; } } else { retCode = E_INVALIDARG; } } return retCode; } // Forces the default action of our element. In simplest cases, send a click. STDMETHODIMP CefIAccessible::accDoDefaultAction(VARIANT varChild) { HRESULT retCode = DATACHECK(node_); if (SUCCEEDED(retCode) && VALID_CHILDID(varChild)) { // doing our default action for out button is to simply click the button. CefRefPtr browser = node_->GetBrowser(); if (browser) { CefMouseEvent mouse_event; const CefRect& rect = node_->AxLocation(); mouse_event.x = MiddleX(rect); mouse_event.y = MiddleY(rect); mouse_event.modifiers = 0; browser->GetHost()->SendMouseClickEvent(mouse_event, MBT_LEFT, false, 1); browser->GetHost()->SendMouseClickEvent(mouse_event, MBT_LEFT, true, 1); } } else { retCode = E_INVALIDARG; } return retCode; } // Set the name for an element in the accessibility tree STDMETHODIMP CefIAccessible::put_accName(VARIANT varChild, BSTR szName) { return E_NOTIMPL; } // Set the value for an element in the accessibility tree STDMETHODIMP CefIAccessible::put_accValue(VARIANT varChild, BSTR szValue) { return E_NOTIMPL; } // Return E_NOTIMPL as no help file/ topic STDMETHODIMP CefIAccessible::get_accHelp(VARIANT varChild, BSTR* pszHelp) { return E_NOTIMPL; } STDMETHODIMP CefIAccessible::get_accHelpTopic(BSTR* pszHelpFile, VARIANT varChild, long* pidTopic) { return E_NOTIMPL; } // IDispatch - We are not going to return E_NOTIMPL from IDispatch methods and // let Active Accessibility implement the IAccessible interface for them. STDMETHODIMP CefIAccessible::GetTypeInfoCount(unsigned int FAR* pctinfo) { return E_NOTIMPL; } STDMETHODIMP CefIAccessible::GetTypeInfo(unsigned int iTInfo, LCID lcid, ITypeInfo FAR* FAR* ppTInfo) { return E_NOTIMPL; } STDMETHODIMP CefIAccessible::GetIDsOfNames(REFIID riid, OLECHAR FAR* FAR* rgszNames, unsigned int cNames, LCID lcid, DISPID FAR* rgDispId) { return E_NOTIMPL; } STDMETHODIMP CefIAccessible::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS FAR* pDispParams, VARIANT FAR* pVarResult, EXCEPINFO FAR* pExcepInfo, unsigned int FAR* puArgErr) { return E_NOTIMPL; } void OsrAXNode::NotifyAccessibilityEvent(std::string event_type) const { if (event_type == "focus") { // Notify Screen Reader of focus change ::NotifyWinEvent(EVENT_OBJECT_FOCUS, GetWindowHandle(), OBJID_CLIENT, node_id_); } } void OsrAXNode::Destroy() { CefIAccessible* ptr = static_cast(platform_accessibility_); if (ptr) ptr->MarkDestroyed(); platform_accessibility_ = NULL; } // Create and return NSAccessibility Implementation Object for Window CefNativeAccessible* OsrAXNode::GetNativeAccessibleObject(OsrAXNode* parent) { if (!platform_accessibility_) { platform_accessibility_ = new CefIAccessible(this); platform_accessibility_->AddRef(); SetParent(parent); } return platform_accessibility_; } } // namespace client #else // !defined(CEF_USE_ATL) namespace client { void OsrAXNode::NotifyAccessibilityEvent(std::string event_type) const { } void OsrAXNode::Destroy() {} CefNativeAccessible* OsrAXNode::GetNativeAccessibleObject(OsrAXNode* parent) { return NULL; } } // namespace client #endif // !defined(CEF_USE_ATL)