/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "AccessibleWrap.h" #include "Accessible-inl.h" #include "Compatibility.h" #include "DocAccessible-inl.h" #include "mozilla/a11y/DocAccessibleParent.h" #include "EnumVariant.h" #include "nsAccUtils.h" #include "nsCoreUtils.h" #include "nsIAccessibleEvent.h" #include "nsWinUtils.h" #include "mozilla/a11y/ProxyAccessible.h" #include "ProxyWrappers.h" #include "ServiceProvider.h" #include "Relation.h" #include "Role.h" #include "RootAccessible.h" #include "sdnAccessible.h" #include "States.h" #ifdef A11Y_LOG #include "Logging.h" #endif #include "nsIMutableArray.h" #include "nsIFrame.h" #include "nsIScrollableFrame.h" #include "mozilla/dom/NodeInfo.h" #include "nsIServiceManager.h" #include "nsNameSpaceManager.h" #include "nsTextFormatter.h" #include "nsView.h" #include "nsViewManager.h" #include "nsEventMap.h" #include "nsArrayUtils.h" #include "mozilla/Preferences.h" #include "oleacc.h" using namespace mozilla; using namespace mozilla::a11y; const uint32_t USE_ROLE_STRING = 0; /* For documentation of the accessibility architecture, * see http://lxr.mozilla.org/seamonkey/source/accessible/accessible-docs.html */ //#define DEBUG_LEAKS #ifdef DEBUG_LEAKS static gAccessibles = 0; #endif #ifdef _WIN64 IDSet AccessibleWrap::sIDGen; #endif static const int32_t kIEnumVariantDisconnected = -1; //////////////////////////////////////////////////////////////////////////////// // AccessibleWrap //////////////////////////////////////////////////////////////////////////////// AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc) : Accessible(aContent, aDoc) #ifdef _WIN64 , mID(kNoID) #endif { } AccessibleWrap::~AccessibleWrap() { #ifdef _WIN64 if (mID != kNoID) sIDGen.ReleaseID(mID); #endif } ITypeInfo* AccessibleWrap::gTypeInfo = nullptr; NS_IMPL_ISUPPORTS_INHERITED0(AccessibleWrap, Accessible) void AccessibleWrap::Shutdown() { #ifdef _WIN64 if (mID != kNoID) static_cast(mDoc)->RemoveID(mID); #endif Accessible::Shutdown(); } //----------------------------------------------------- // IUnknown interface methods - see iunknown.h for documentation //----------------------------------------------------- // Microsoft COM QueryInterface STDMETHODIMP AccessibleWrap::QueryInterface(REFIID iid, void** ppv) { A11Y_TRYBLOCK_BEGIN if (!ppv) return E_INVALIDARG; *ppv = nullptr; if (IID_IUnknown == iid) *ppv = static_cast(this); else if (IID_IDispatch == iid || IID_IAccessible == iid) *ppv = static_cast(this); else if (IID_IEnumVARIANT == iid && !IsProxy()) { // Don't support this interface for leaf elements. if (!HasChildren() || nsAccUtils::MustPrune(this)) return E_NOINTERFACE; *ppv = static_cast(new ChildrenEnumVariant(this)); } else if (IID_IServiceProvider == iid) *ppv = new ServiceProvider(this); else if (IID_ISimpleDOMNode == iid && !IsProxy()) { if (IsDefunct() || (!HasOwnContent() && !IsDoc())) return E_NOINTERFACE; *ppv = static_cast(new sdnAccessible(GetNode())); } if (nullptr == *ppv) { HRESULT hr = ia2Accessible::QueryInterface(iid, ppv); if (SUCCEEDED(hr)) return hr; } if (nullptr == *ppv && !IsProxy()) { HRESULT hr = ia2AccessibleComponent::QueryInterface(iid, ppv); if (SUCCEEDED(hr)) return hr; } if (nullptr == *ppv) { HRESULT hr = ia2AccessibleHyperlink::QueryInterface(iid, ppv); if (SUCCEEDED(hr)) return hr; } if (nullptr == *ppv && !IsProxy()) { HRESULT hr = ia2AccessibleValue::QueryInterface(iid, ppv); if (SUCCEEDED(hr)) return hr; } if (nullptr == *ppv) return E_NOINTERFACE; (reinterpret_cast(*ppv))->AddRef(); return S_OK; A11Y_TRYBLOCK_END } //----------------------------------------------------- // IAccessible methods //----------------------------------------------------- STDMETHODIMP AccessibleWrap::get_accParent( IDispatch __RPC_FAR *__RPC_FAR *ppdispParent) { A11Y_TRYBLOCK_BEGIN if (!ppdispParent) return E_INVALIDARG; *ppdispParent = nullptr; if (IsDefunct()) return CO_E_OBJNOTCONNECTED; if (IsProxy()) { ProxyAccessible* proxy = Proxy(); ProxyAccessible* parent = proxy->Parent(); if (!parent) return S_FALSE; *ppdispParent = NativeAccessible(WrapperFor(parent)); return S_OK; } DocAccessible* doc = AsDoc(); if (doc) { // Return window system accessible object for root document and tab document // accessibles. if (!doc->ParentDocument() || (nsWinUtils::IsWindowEmulationStarted() && nsCoreUtils::IsTabDocument(doc->DocumentNode()))) { HWND hwnd = static_cast(doc->GetNativeWindow()); if (hwnd && SUCCEEDED(::AccessibleObjectFromWindow(hwnd, OBJID_WINDOW, IID_IAccessible, (void**)ppdispParent))) { return S_OK; } } } Accessible* xpParentAcc = Parent(); if (!xpParentAcc) return S_FALSE; *ppdispParent = NativeAccessible(xpParentAcc); return S_OK; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleWrap::get_accChildCount( long __RPC_FAR *pcountChildren) { A11Y_TRYBLOCK_BEGIN if (!pcountChildren) return E_INVALIDARG; *pcountChildren = 0; if (IsDefunct()) return CO_E_OBJNOTCONNECTED; if (IsProxy()) { ProxyAccessible* proxy = Proxy(); if (proxy->MustPruneChildren()) return S_OK; *pcountChildren = proxy->ChildrenCount(); return S_OK; } if (nsAccUtils::MustPrune(this)) return S_OK; *pcountChildren = ChildCount(); return S_OK; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleWrap::get_accChild( /* [in] */ VARIANT varChild, /* [retval][out] */ IDispatch __RPC_FAR *__RPC_FAR *ppdispChild) { A11Y_TRYBLOCK_BEGIN if (!ppdispChild) return E_INVALIDARG; *ppdispChild = nullptr; if (IsDefunct()) return CO_E_OBJNOTCONNECTED; // IAccessible::accChild is used to return this accessible or child accessible // at the given index or to get an accessible by child ID in the case of // document accessible (it's handled by overriden GetXPAccessibleFor method // on the document accessible). The getting an accessible by child ID is used // by AccessibleObjectFromEvent() called by AT when AT handles our MSAA event. Accessible* child = GetXPAccessibleFor(varChild); if (!child) return E_INVALIDARG; if (child->IsDefunct()) return CO_E_OBJNOTCONNECTED; *ppdispChild = NativeAccessible(child); return S_OK; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleWrap::get_accName( /* [optional][in] */ VARIANT varChild, /* [retval][out] */ BSTR __RPC_FAR *pszName) { A11Y_TRYBLOCK_BEGIN if (!pszName) return E_INVALIDARG; *pszName = nullptr; if (IsDefunct()) return CO_E_OBJNOTCONNECTED; Accessible* xpAccessible = GetXPAccessibleFor(varChild); if (!xpAccessible) return E_INVALIDARG; if (xpAccessible->IsDefunct()) return CO_E_OBJNOTCONNECTED; nsAutoString name; if (xpAccessible->IsProxy()) xpAccessible->Proxy()->Name(name); else xpAccessible->Name(name); // The name was not provided, e.g. no alt attribute for an image. A screen // reader may choose to invent its own accessible name, e.g. from an image src // attribute. Refer to eNoNameOnPurpose return value. if (name.IsVoid()) return S_FALSE; *pszName = ::SysAllocStringLen(name.get(), name.Length()); if (!*pszName) return E_OUTOFMEMORY; return S_OK; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleWrap::get_accValue( /* [optional][in] */ VARIANT varChild, /* [retval][out] */ BSTR __RPC_FAR *pszValue) { A11Y_TRYBLOCK_BEGIN if (!pszValue) return E_INVALIDARG; *pszValue = nullptr; if (IsDefunct()) return CO_E_OBJNOTCONNECTED; Accessible* xpAccessible = GetXPAccessibleFor(varChild); if (!xpAccessible) return E_INVALIDARG; if (xpAccessible->IsDefunct()) return CO_E_OBJNOTCONNECTED; // TODO make this work with proxies. if (IsProxy()) return E_NOTIMPL; nsAutoString value; xpAccessible->Value(value); // See bug 438784: need to expose URL on doc's value attribute. For this, // reverting part of fix for bug 425693 to make this MSAA method behave // IAccessible2-style. if (value.IsEmpty()) return S_FALSE; *pszValue = ::SysAllocStringLen(value.get(), value.Length()); if (!*pszValue) return E_OUTOFMEMORY; return S_OK; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleWrap::get_accDescription(VARIANT varChild, BSTR __RPC_FAR *pszDescription) { A11Y_TRYBLOCK_BEGIN if (!pszDescription) return E_INVALIDARG; *pszDescription = nullptr; if (IsDefunct()) return CO_E_OBJNOTCONNECTED; Accessible* xpAccessible = GetXPAccessibleFor(varChild); if (!xpAccessible) return E_INVALIDARG; if (xpAccessible->IsDefunct()) return CO_E_OBJNOTCONNECTED; nsAutoString description; if (IsProxy()) xpAccessible->Proxy()->Description(description); else xpAccessible->Description(description); *pszDescription = ::SysAllocStringLen(description.get(), description.Length()); return *pszDescription ? S_OK : E_OUTOFMEMORY; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleWrap::get_accRole( /* [optional][in] */ VARIANT varChild, /* [retval][out] */ VARIANT __RPC_FAR *pvarRole) { A11Y_TRYBLOCK_BEGIN if (!pvarRole) return E_INVALIDARG; VariantInit(pvarRole); if (IsDefunct()) return CO_E_OBJNOTCONNECTED; Accessible* xpAccessible = GetXPAccessibleFor(varChild); if (!xpAccessible) return E_INVALIDARG; if (xpAccessible->IsDefunct()) return CO_E_OBJNOTCONNECTED; a11y::role geckoRole; if (xpAccessible->IsProxy()) { geckoRole = xpAccessible->Proxy()->Role(); } else { #ifdef DEBUG NS_ASSERTION(nsAccUtils::IsTextInterfaceSupportCorrect(xpAccessible), "Does not support Text when it should"); #endif geckoRole = xpAccessible->Role(); } uint32_t msaaRole = 0; #define ROLE(_geckoRole, stringRole, atkRole, macRole, \ _msaaRole, ia2Role, nameRule) \ case roles::_geckoRole: \ msaaRole = _msaaRole; \ break; switch (geckoRole) { #include "RoleMap.h" default: MOZ_CRASH("Unknown role."); }; #undef ROLE // Special case, if there is a ROLE_ROW inside of a ROLE_TREE_TABLE, then call the MSAA role // a ROLE_OUTLINEITEM for consistency and compatibility. // We need this because ARIA has a role of "row" for both grid and treegrid if (xpAccessible->IsProxy()) { if (geckoRole == roles::ROW && xpAccessible->Proxy()->Parent()->Role() == roles::TREE_TABLE) msaaRole = ROLE_SYSTEM_OUTLINEITEM; } else { if (geckoRole == roles::ROW) { Accessible* xpParent = Parent(); if (xpParent && xpParent->Role() == roles::TREE_TABLE) msaaRole = ROLE_SYSTEM_OUTLINEITEM; } } // -- Try enumerated role if (msaaRole != USE_ROLE_STRING) { pvarRole->vt = VT_I4; pvarRole->lVal = msaaRole; // Normal enumerated role return S_OK; } // XXX bug 798492 make this work with proxies? if (IsProxy()) return E_FAIL; // -- Try BSTR role // Could not map to known enumerated MSAA role like ROLE_BUTTON // Use BSTR role to expose role attribute or tag name + namespace nsIContent *content = xpAccessible->GetContent(); if (!content) return E_FAIL; if (content->IsElement()) { nsAutoString roleString; if (msaaRole != ROLE_SYSTEM_CLIENT && !content->GetAttr(kNameSpaceID_None, nsGkAtoms::role, roleString)) { nsIDocument * document = content->GetCurrentDoc(); if (!document) return E_FAIL; dom::NodeInfo *nodeInfo = content->NodeInfo(); nodeInfo->GetName(roleString); // Only append name space if different from that of current document. if (!nodeInfo->NamespaceEquals(document->GetDefaultNamespaceID())) { nsAutoString nameSpaceURI; nodeInfo->GetNamespaceURI(nameSpaceURI); roleString += NS_LITERAL_STRING(", ") + nameSpaceURI; } } if (!roleString.IsEmpty()) { pvarRole->vt = VT_BSTR; pvarRole->bstrVal = ::SysAllocString(roleString.get()); return S_OK; } } return E_FAIL; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleWrap::get_accState( /* [optional][in] */ VARIANT varChild, /* [retval][out] */ VARIANT __RPC_FAR *pvarState) { A11Y_TRYBLOCK_BEGIN if (!pvarState) return E_INVALIDARG; VariantInit(pvarState); pvarState->vt = VT_I4; pvarState->lVal = 0; if (IsDefunct()) return CO_E_OBJNOTCONNECTED; Accessible* xpAccessible = GetXPAccessibleFor(varChild); if (!xpAccessible) return E_INVALIDARG; if (xpAccessible->IsDefunct()) return CO_E_OBJNOTCONNECTED; // MSAA only has 31 states and the lowest 31 bits of our state bit mask // are the same states as MSAA. // Note: we map the following Gecko states to different MSAA states: // REQUIRED -> ALERT_LOW // ALERT -> ALERT_MEDIUM // INVALID -> ALERT_HIGH // CHECKABLE -> MARQUEED uint64_t state; if (xpAccessible->IsProxy()) state = xpAccessible->Proxy()->State(); else state = State(); uint32_t msaaState = 0; nsAccUtils::To32States(state, &msaaState, nullptr); pvarState->lVal = msaaState; return S_OK; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleWrap::get_accHelp( /* [optional][in] */ VARIANT varChild, /* [retval][out] */ BSTR __RPC_FAR *pszHelp) { A11Y_TRYBLOCK_BEGIN if (!pszHelp) return E_INVALIDARG; *pszHelp = nullptr; return S_FALSE; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleWrap::get_accHelpTopic( /* [out] */ BSTR __RPC_FAR *pszHelpFile, /* [optional][in] */ VARIANT varChild, /* [retval][out] */ long __RPC_FAR *pidTopic) { A11Y_TRYBLOCK_BEGIN if (!pszHelpFile || !pidTopic) return E_INVALIDARG; *pszHelpFile = nullptr; *pidTopic = 0; return S_FALSE; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleWrap::get_accKeyboardShortcut( /* [optional][in] */ VARIANT varChild, /* [retval][out] */ BSTR __RPC_FAR *pszKeyboardShortcut) { A11Y_TRYBLOCK_BEGIN if (!pszKeyboardShortcut) return E_INVALIDARG; *pszKeyboardShortcut = nullptr; if (IsDefunct()) return CO_E_OBJNOTCONNECTED; Accessible* acc = GetXPAccessibleFor(varChild); if (!acc) return E_INVALIDARG; if (acc->IsDefunct()) return CO_E_OBJNOTCONNECTED; // TODO make this work with proxies. if (acc->IsProxy()) return E_NOTIMPL; KeyBinding keyBinding = acc->AccessKey(); if (keyBinding.IsEmpty()) keyBinding = acc->KeyboardShortcut(); nsAutoString shortcut; keyBinding.ToString(shortcut); *pszKeyboardShortcut = ::SysAllocStringLen(shortcut.get(), shortcut.Length()); return *pszKeyboardShortcut ? S_OK : E_OUTOFMEMORY; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleWrap::get_accFocus( /* [retval][out] */ VARIANT __RPC_FAR *pvarChild) { A11Y_TRYBLOCK_BEGIN if (!pvarChild) return E_INVALIDARG; VariantInit(pvarChild); // VT_EMPTY: None. This object does not have the keyboard focus itself // and does not contain a child that has the keyboard focus. // VT_I4: lVal is CHILDID_SELF. The object itself has the keyboard focus. // VT_I4: lVal contains the child ID of the child element with the keyboard focus. // VT_DISPATCH: pdispVal member is the address of the IDispatch interface // for the child object with the keyboard focus. if (IsDefunct()) return CO_E_OBJNOTCONNECTED; // Return the current IAccessible child that has focus Accessible* focusedAccessible; if (IsProxy()) { ProxyAccessible* proxy = Proxy()->FocusedChild(); focusedAccessible = proxy ? WrapperFor(proxy) : nullptr; } else { focusedAccessible = FocusedChild(); } if (focusedAccessible == this) { pvarChild->vt = VT_I4; pvarChild->lVal = CHILDID_SELF; } else if (focusedAccessible) { pvarChild->vt = VT_DISPATCH; pvarChild->pdispVal = NativeAccessible(focusedAccessible); } else { pvarChild->vt = VT_EMPTY; // No focus or focus is not a child } return S_OK; A11Y_TRYBLOCK_END } /** * This helper class implements IEnumVARIANT for a nsTArray containing * accessible objects. */ class AccessibleEnumerator final : public IEnumVARIANT { public: AccessibleEnumerator(const nsTArray& aArray) : mArray(aArray), mCurIndex(0) { } AccessibleEnumerator(const AccessibleEnumerator& toCopy) : mArray(toCopy.mArray), mCurIndex(toCopy.mCurIndex) { } ~AccessibleEnumerator() { } // IUnknown DECL_IUNKNOWN // IEnumVARIANT STDMETHODIMP Next(unsigned long celt, VARIANT FAR* rgvar, unsigned long FAR* pceltFetched); STDMETHODIMP Skip(unsigned long celt); STDMETHODIMP Reset() { mCurIndex = 0; return S_OK; } STDMETHODIMP Clone(IEnumVARIANT FAR* FAR* ppenum); private: nsTArray mArray; uint32_t mCurIndex; }; STDMETHODIMP AccessibleEnumerator::QueryInterface(REFIID iid, void ** ppvObject) { A11Y_TRYBLOCK_BEGIN if (iid == IID_IEnumVARIANT) { *ppvObject = static_cast(this); AddRef(); return S_OK; } if (iid == IID_IUnknown) { *ppvObject = static_cast(this); AddRef(); return S_OK; } *ppvObject = nullptr; return E_NOINTERFACE; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleEnumerator::Next(unsigned long celt, VARIANT FAR* rgvar, unsigned long FAR* pceltFetched) { A11Y_TRYBLOCK_BEGIN uint32_t length = mArray.Length(); HRESULT hr = S_OK; // Can't get more elements than there are... if (celt > length - mCurIndex) { hr = S_FALSE; celt = length - mCurIndex; } // Copy the elements of the array into rgvar. for (uint32_t i = 0; i < celt; ++i, ++mCurIndex) { rgvar[i].vt = VT_DISPATCH; rgvar[i].pdispVal = AccessibleWrap::NativeAccessible(mArray[mCurIndex]); } if (pceltFetched) *pceltFetched = celt; return hr; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleEnumerator::Clone(IEnumVARIANT FAR* FAR* ppenum) { A11Y_TRYBLOCK_BEGIN *ppenum = new AccessibleEnumerator(*this); if (!*ppenum) return E_OUTOFMEMORY; NS_ADDREF(*ppenum); return S_OK; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleEnumerator::Skip(unsigned long celt) { A11Y_TRYBLOCK_BEGIN uint32_t length = mArray.Length(); // Check if we can skip the requested number of elements if (celt > length - mCurIndex) { mCurIndex = length; return S_FALSE; } mCurIndex += celt; return S_OK; A11Y_TRYBLOCK_END } /** * This method is called when a client wants to know which children of a node * are selected. Note that this method can only find selected children for * accessible object which implement SelectAccessible. * * The VARIANT return value arguement is expected to either contain a single IAccessible * or an IEnumVARIANT of IAccessibles. We return the IEnumVARIANT regardless of the number * of children selected, unless there are none selected in which case we return an empty * VARIANT. * * We get the selected options from the select's accessible object and wrap * those in an AccessibleEnumerator which we then put in the return VARIANT. * * returns a VT_EMPTY VARIANT if: * - there are no selected children for this object * - the object is not the type that can have children selected */ STDMETHODIMP AccessibleWrap::get_accSelection(VARIANT __RPC_FAR *pvarChildren) { A11Y_TRYBLOCK_BEGIN if (!pvarChildren) return E_INVALIDARG; VariantInit(pvarChildren); pvarChildren->vt = VT_EMPTY; if (IsDefunct()) return CO_E_OBJNOTCONNECTED; // TODO make this work with proxies. if (IsProxy()) return E_NOTIMPL; if (IsSelect()) { nsAutoTArray selectedItems; if (IsProxy()) { nsTArray proxies; Proxy()->SelectedItems(&proxies); uint32_t selectedCount = proxies.Length(); for (uint32_t i = 0; i < selectedCount; i++) { selectedItems.AppendElement(WrapperFor(proxies[i])); } } else { SelectedItems(&selectedItems); } // 1) Create and initialize the enumeration RefPtr pEnum = new AccessibleEnumerator(selectedItems); pvarChildren->vt = VT_UNKNOWN; // this must be VT_UNKNOWN for an IEnumVARIANT NS_ADDREF(pvarChildren->punkVal = pEnum); } return S_OK; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleWrap::get_accDefaultAction( /* [optional][in] */ VARIANT varChild, /* [retval][out] */ BSTR __RPC_FAR *pszDefaultAction) { A11Y_TRYBLOCK_BEGIN if (!pszDefaultAction) return E_INVALIDARG; *pszDefaultAction = nullptr; if (IsDefunct()) return CO_E_OBJNOTCONNECTED; Accessible* xpAccessible = GetXPAccessibleFor(varChild); if (!xpAccessible) return E_INVALIDARG; if (xpAccessible->IsDefunct()) return CO_E_OBJNOTCONNECTED; nsAutoString defaultAction; if (xpAccessible->IsProxy()) { xpAccessible->Proxy()->ActionNameAt(0, defaultAction); } else { xpAccessible->ActionNameAt(0, defaultAction); } *pszDefaultAction = ::SysAllocStringLen(defaultAction.get(), defaultAction.Length()); return *pszDefaultAction ? S_OK : E_OUTOFMEMORY; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleWrap::accSelect( /* [in] */ long flagsSelect, /* [optional][in] */ VARIANT varChild) { A11Y_TRYBLOCK_BEGIN if (IsDefunct()) return CO_E_OBJNOTCONNECTED; // currently only handle focus and selection Accessible* xpAccessible = GetXPAccessibleFor(varChild); if (!xpAccessible) return E_INVALIDARG; if (xpAccessible->IsDefunct()) return CO_E_OBJNOTCONNECTED; if (flagsSelect & SELFLAG_TAKEFOCUS) { if (xpAccessible->IsProxy()) { xpAccessible->Proxy()->TakeFocus(); } else { xpAccessible->TakeFocus(); } return S_OK; } if (flagsSelect & SELFLAG_TAKESELECTION) { if (xpAccessible->IsProxy()) { xpAccessible->Proxy()->TakeSelection(); } else { xpAccessible->TakeSelection(); } return S_OK; } if (flagsSelect & SELFLAG_ADDSELECTION) { if (xpAccessible->IsProxy()) { xpAccessible->Proxy()->SetSelected(true); } else { xpAccessible->SetSelected(true); } return S_OK; } if (flagsSelect & SELFLAG_REMOVESELECTION) { if (xpAccessible->IsProxy()) { xpAccessible->Proxy()->SetSelected(false); } else { xpAccessible->SetSelected(false); } return S_OK; } return E_FAIL; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleWrap::accLocation( /* [out] */ long __RPC_FAR *pxLeft, /* [out] */ long __RPC_FAR *pyTop, /* [out] */ long __RPC_FAR *pcxWidth, /* [out] */ long __RPC_FAR *pcyHeight, /* [optional][in] */ VARIANT varChild) { A11Y_TRYBLOCK_BEGIN if (!pxLeft || !pyTop || !pcxWidth || !pcyHeight) return E_INVALIDARG; *pxLeft = 0; *pyTop = 0; *pcxWidth = 0; *pcyHeight = 0; if (IsDefunct()) return CO_E_OBJNOTCONNECTED; Accessible* xpAccessible = GetXPAccessibleFor(varChild); if (!xpAccessible) return E_INVALIDARG; if (xpAccessible->IsDefunct()) return CO_E_OBJNOTCONNECTED; nsIntRect rect; if (xpAccessible->IsProxy()) { rect = xpAccessible->Proxy()->Bounds(); } else { rect = xpAccessible->Bounds(); } *pxLeft = rect.x; *pyTop = rect.y; *pcxWidth = rect.width; *pcyHeight = rect.height; return S_OK; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleWrap::accNavigate( /* [in] */ long navDir, /* [optional][in] */ VARIANT varStart, /* [retval][out] */ VARIANT __RPC_FAR *pvarEndUpAt) { A11Y_TRYBLOCK_BEGIN if (!pvarEndUpAt) return E_INVALIDARG; VariantInit(pvarEndUpAt); if (IsDefunct()) return CO_E_OBJNOTCONNECTED; Accessible* accessible = GetXPAccessibleFor(varStart); if (!accessible) return E_INVALIDARG; if (accessible->IsDefunct()) return CO_E_OBJNOTCONNECTED; Accessible* navAccessible = nullptr; Maybe xpRelation; #define RELATIONTYPE(geckoType, stringType, atkType, msaaType, ia2Type) \ case msaaType: \ xpRelation.emplace(RelationType::geckoType); \ break; switch(navDir) { case NAVDIR_FIRSTCHILD: if (accessible->IsProxy()) { if (!accessible->Proxy()->MustPruneChildren()) { navAccessible = WrapperFor(accessible->Proxy()->FirstChild()); } } else { if (!nsAccUtils::MustPrune(accessible)) navAccessible = accessible->FirstChild(); } break; case NAVDIR_LASTCHILD: if (accessible->IsProxy()) { if (!accessible->Proxy()->MustPruneChildren()) { navAccessible = WrapperFor(accessible->Proxy()->LastChild()); } } else { if (!nsAccUtils::MustPrune(accessible)) navAccessible = accessible->LastChild(); } break; case NAVDIR_NEXT: navAccessible = accessible->IsProxy() ? WrapperFor(accessible->Proxy()->NextSibling()) : accessible->NextSibling(); break; case NAVDIR_PREVIOUS: navAccessible = accessible->IsProxy() ? WrapperFor(accessible->Proxy()->PrevSibling()) : accessible->PrevSibling(); break; case NAVDIR_DOWN: case NAVDIR_LEFT: case NAVDIR_RIGHT: case NAVDIR_UP: return E_NOTIMPL; // MSAA relationship extensions to accNavigate #include "RelationTypeMap.h" default: return E_INVALIDARG; } #undef RELATIONTYPE pvarEndUpAt->vt = VT_EMPTY; if (xpRelation) { if (accessible->IsProxy()) { nsTArray targets = accessible->Proxy()->RelationByType(*xpRelation); if (targets.Length()) { navAccessible = WrapperFor(targets[0]); } } else { Relation rel = RelationByType(*xpRelation); navAccessible = rel.Next(); } } if (!navAccessible) return E_FAIL; pvarEndUpAt->pdispVal = NativeAccessible(navAccessible); pvarEndUpAt->vt = VT_DISPATCH; return S_OK; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleWrap::accHitTest( /* [in] */ long xLeft, /* [in] */ long yTop, /* [retval][out] */ VARIANT __RPC_FAR *pvarChild) { A11Y_TRYBLOCK_BEGIN if (!pvarChild) return E_INVALIDARG; VariantInit(pvarChild); if (IsDefunct()) return CO_E_OBJNOTCONNECTED; Accessible* accessible = nullptr; if (IsProxy()) { ProxyAccessible* proxy = Proxy()->ChildAtPoint(xLeft, yTop, eDirectChild); if (proxy) { accessible = WrapperFor(proxy); } } else { accessible = ChildAtPoint(xLeft, yTop, eDirectChild); } // if we got a child if (accessible) { // if the child is us if (accessible == this) { pvarChild->vt = VT_I4; pvarChild->lVal = CHILDID_SELF; } else { // its not create an Accessible for it. pvarChild->vt = VT_DISPATCH; pvarChild->pdispVal = NativeAccessible(accessible); } } else { // no child at that point pvarChild->vt = VT_EMPTY; return S_FALSE; } return S_OK; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleWrap::accDoDefaultAction( /* [optional][in] */ VARIANT varChild) { A11Y_TRYBLOCK_BEGIN if (IsDefunct()) return CO_E_OBJNOTCONNECTED; Accessible* xpAccessible = GetXPAccessibleFor(varChild); if (!xpAccessible) return E_INVALIDARG; if (xpAccessible->IsDefunct()) return CO_E_OBJNOTCONNECTED; // TODO make this work with proxies. if (xpAccessible->IsProxy()) return xpAccessible->Proxy()->DoAction(0) ? S_OK : E_INVALIDARG; return xpAccessible->DoAction(0) ? S_OK : E_INVALIDARG; A11Y_TRYBLOCK_END } STDMETHODIMP AccessibleWrap::put_accName( /* [optional][in] */ VARIANT varChild, /* [in] */ BSTR szName) { return E_NOTIMPL; } STDMETHODIMP AccessibleWrap::put_accValue( /* [optional][in] */ VARIANT varChild, /* [in] */ BSTR szValue) { return E_NOTIMPL; } //////////////////////////////////////////////////////////////////////////////// // IDispatch STDMETHODIMP AccessibleWrap::GetTypeInfoCount(UINT *pctinfo) { if (!pctinfo) return E_INVALIDARG; *pctinfo = 1; return S_OK; } STDMETHODIMP AccessibleWrap::GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo) { if (!ppTInfo) return E_INVALIDARG; *ppTInfo = nullptr; if (iTInfo != 0) return DISP_E_BADINDEX; ITypeInfo * typeInfo = GetTI(lcid); if (!typeInfo) return E_FAIL; typeInfo->AddRef(); *ppTInfo = typeInfo; return S_OK; } STDMETHODIMP AccessibleWrap::GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId) { ITypeInfo *typeInfo = GetTI(lcid); if (!typeInfo) return E_FAIL; HRESULT hr = DispGetIDsOfNames(typeInfo, rgszNames, cNames, rgDispId); return hr; } STDMETHODIMP AccessibleWrap::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr) { ITypeInfo *typeInfo = GetTI(lcid); if (!typeInfo) return E_FAIL; return typeInfo->Invoke(static_cast(this), dispIdMember, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr); } void AccessibleWrap::GetNativeInterface(void** aOutAccessible) { *aOutAccessible = static_cast(this); NS_ADDREF_THIS(); } void AccessibleWrap::FireWinEvent(Accessible* aTarget, uint32_t aEventType) { static_assert(sizeof(gWinEventMap)/sizeof(gWinEventMap[0]) == nsIAccessibleEvent::EVENT_LAST_ENTRY, "MSAA event map skewed"); NS_ASSERTION(aEventType > 0 && aEventType < ArrayLength(gWinEventMap), "invalid event type"); uint32_t winEvent = gWinEventMap[aEventType]; if (!winEvent) return; int32_t childID = GetChildIDFor(aTarget); if (!childID) return; // Can't fire an event without a child ID HWND hwnd = GetHWNDFor(aTarget); if (!hwnd) { return; } // Fire MSAA event for client area window. ::NotifyWinEvent(winEvent, hwnd, OBJID_CLIENT, childID); // JAWS announces collapsed combobox navigation based on focus events. if (aEventType == nsIAccessibleEvent::EVENT_SELECTION && Compatibility::IsJAWS()) { roles::Role role = aTarget->IsProxy() ? aTarget->Proxy()->Role() : aTarget->Role(); if (role == roles::COMBOBOX_OPTION) { ::NotifyWinEvent(EVENT_OBJECT_FOCUS, hwnd, OBJID_CLIENT, childID); } } } //////////////////////////////////////////////////////////////////////////////// // Accessible nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) { nsresult rv = Accessible::HandleAccEvent(aEvent); NS_ENSURE_SUCCESS(rv, rv); if (IPCAccessibilityActive()) { return NS_OK; } uint32_t eventType = aEvent->GetEventType(); // Means we're not active. NS_ENSURE_TRUE(!IsDefunct(), NS_ERROR_FAILURE); Accessible* accessible = aEvent->GetAccessible(); if (!accessible) return NS_OK; if (eventType == nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED || eventType == nsIAccessibleEvent::EVENT_FOCUS) { UpdateSystemCaretFor(accessible); } FireWinEvent(accessible, eventType); return NS_OK; } DocProxyAccessibleWrap* AccessibleWrap::DocProxyWrapper() const { MOZ_ASSERT(IsProxy()); ProxyAccessible* proxy = Proxy(); if (!proxy) { return nullptr; } AccessibleWrap* acc = WrapperFor(proxy->Document()); MOZ_ASSERT(acc->IsDoc()); return static_cast(acc); } //////////////////////////////////////////////////////////////////////////////// // AccessibleWrap //------- Helper methods --------- int32_t AccessibleWrap::GetChildIDFor(Accessible* aAccessible) { // A child ID of the window is required, when we use NotifyWinEvent, // so that the 3rd party application can call back and get the IAccessible // the event occurred on. if (!aAccessible) { return 0; } #ifdef _WIN64 if (!aAccessible->Document() && !aAccessible->IsProxy()) return 0; uint32_t* id = & static_cast(aAccessible)->mID; if (*id != kNoID) return *id; *id = sIDGen.GetID(); if (aAccessible->IsProxy()) { DocProxyAccessibleWrap* doc = static_cast(aAccessible)->DocProxyWrapper(); doc->AddID(*id, static_cast(aAccessible)); } else { DocAccessibleWrap* doc = static_cast(aAccessible->Document()); doc->AddID(*id, static_cast(aAccessible)); } return *id; #else int32_t id = - reinterpret_cast(aAccessible); if (aAccessible->IsProxy()) { DocProxyAccessibleWrap* doc = static_cast(aAccessible)->DocProxyWrapper(); doc->AddID(id, static_cast(aAccessible)); } return id; #endif } HWND AccessibleWrap::GetHWNDFor(Accessible* aAccessible) { if (!aAccessible) { return nullptr; } // Accessibles in child processes are said to have the HWND of the window // their tab is within. Popups are always in the parent process, and so // never proxied, which means this is basically correct. if (aAccessible->IsProxy()) { ProxyAccessible* proxy = aAccessible->Proxy(); if (!proxy) { return nullptr; } Accessible* outerDoc = proxy->OuterDocOfRemoteBrowser(); NS_ASSERTION(outerDoc, "no outer doc for accessible remote tab!"); if (!outerDoc) { return nullptr; } return GetHWNDFor(outerDoc); } DocAccessible* document = aAccessible->Document(); if(!document) return nullptr; // Popup lives in own windows, use its HWND until the popup window is // hidden to make old JAWS versions work with collapsed comboboxes (see // discussion in bug 379678). nsIFrame* frame = aAccessible->GetFrame(); if (frame) { nsIWidget* widget = frame->GetNearestWidget(); if (widget && widget->IsVisible()) { nsIPresShell* shell = document->PresShell(); nsViewManager* vm = shell->GetViewManager(); if (vm) { nsCOMPtr rootWidget; vm->GetRootWidget(getter_AddRefs(rootWidget)); // Make sure the accessible belongs to popup. If not then use // document HWND (which might be different from root widget in the // case of window emulation). if (rootWidget != widget) return static_cast(widget->GetNativeData(NS_NATIVE_WINDOW)); } } } return static_cast(document->GetNativeWindow()); } IDispatch* AccessibleWrap::NativeAccessible(Accessible* aAccessible) { if (!aAccessible) { NS_WARNING("Not passing in an aAccessible"); return nullptr; } IAccessible* msaaAccessible = nullptr; aAccessible->GetNativeInterface(reinterpret_cast(&msaaAccessible)); return static_cast(msaaAccessible); } #ifdef _WIN64 static Accessible* GetAccessibleInSubtree(DocAccessible* aDoc, uint32_t aID) { Accessible* child = static_cast(aDoc)->GetAccessibleByID(aID); if (child) return child; uint32_t childDocCount = aDoc->ChildDocumentCount(); for (uint32_t i = 0; i < childDocCount; i++) { child = GetAccessibleInSubtree(aDoc->GetChildDocumentAt(i), aID); if (child) return child; } return nullptr; } #endif static AccessibleWrap* GetProxiedAccessibleInSubtree(const DocAccessibleParent* aDoc, uint32_t aID) { auto wrapper = static_cast(WrapperFor(aDoc)); AccessibleWrap* child = wrapper->GetAccessibleByID(aID); if (child) { return child; } size_t childDocs = aDoc->ChildDocCount(); for (size_t i = 0; i < childDocs; i++) { const DocAccessibleParent* childDoc = aDoc->ChildDocAt(i); child = GetProxiedAccessibleInSubtree(childDoc, aID); if (child) { return child; } } return nullptr; } Accessible* AccessibleWrap::GetXPAccessibleFor(const VARIANT& aVarChild) { if (aVarChild.vt != VT_I4) return nullptr; // if its us real easy - this seems to always be the case if (aVarChild.lVal == CHILDID_SELF) return this; if (IsProxy() ? Proxy()->MustPruneChildren() : nsAccUtils::MustPrune(this)) { return nullptr; } if (aVarChild.lVal > 0) { // Gecko child indices are 0-based in contrast to indices used in MSAA. if (IsProxy()) { return WrapperFor(Proxy()->ChildAt(aVarChild.lVal - 1)); } else { return GetChildAt(aVarChild.lVal - 1); } } // If lVal negative then it is treated as child ID and we should look for // accessible through whole accessible subtree including subdocuments. // Otherwise we treat lVal as index in parent. // Convert child ID to unique ID. // First handle the case that both this accessible and the id'd one are in // this process. if (!IsProxy()) { void* uniqueID = reinterpret_cast(-aVarChild.lVal); DocAccessible* document = Document(); Accessible* child = #ifdef _WIN64 GetAccessibleInSubtree(document, static_cast(aVarChild.lVal)); #else document->GetAccessibleByUniqueIDInSubtree(uniqueID); #endif // If it is a document then just return an accessible. if (child && IsDoc()) return child; // Otherwise check whether the accessible is a child (this path works for // ARIA documents and popups). Accessible* parent = child; while (parent && parent != document) { if (parent == this) return child; parent = parent->Parent(); } } // Now see about the case that both this accessible and the target one are // proxied. uint32_t id = aVarChild.lVal; if (IsProxy()) { DocAccessibleParent* proxyDoc = Proxy()->Document(); AccessibleWrap* wrapper = GetProxiedAccessibleInSubtree(proxyDoc, id); if (!wrapper) return nullptr; MOZ_ASSERT(wrapper->IsProxy()); if (proxyDoc == this->Proxy()) { return wrapper; } ProxyAccessible* parent = wrapper->Proxy(); while (parent && parent != proxyDoc) { if (parent == this->Proxy()) { return wrapper; } parent = parent->Parent(); } return nullptr; } // Finally we need to handle the case that this accessible is in the main // process, but the target is proxied. This is the case when the target // accessible is in a child document of this one. DocAccessibleParent* proxyDoc = nullptr; DocAccessible* doc = Document(); const nsTArray* remoteDocs = DocManager::TopLevelRemoteDocs(); if (!remoteDocs) { return nullptr; } size_t docCount = remoteDocs->Length(); for (size_t i = 0; i < docCount; i++) { Accessible* outerDoc = remoteDocs->ElementAt(i)->OuterDocOfRemoteBrowser(); if (!outerDoc) { continue; } if (outerDoc->Document() != doc) { continue; } if (doc == this) { AccessibleWrap* proxyWrapper = GetProxiedAccessibleInSubtree(remoteDocs->ElementAt(i), id); if (proxyWrapper) { return proxyWrapper; } continue; } Accessible* parent = outerDoc; while (parent && parent != doc) { if (parent == this) { AccessibleWrap* proxyWrapper = GetProxiedAccessibleInSubtree(remoteDocs->ElementAt(i), id); if (proxyWrapper) { return proxyWrapper; } } parent = parent->Parent(); } } return nullptr; } void AccessibleWrap::UpdateSystemCaretFor(Accessible* aAccessible) { // Move the system caret so that Windows Tablet Edition and tradional ATs with // off-screen model can follow the caret ::DestroyCaret(); HyperTextAccessible* text = aAccessible->AsHyperText(); if (!text) return; nsIWidget* widget = nullptr; LayoutDeviceIntRect caretRect = text->GetCaretRect(&widget); HWND caretWnd; if (caretRect.IsEmpty() || !(caretWnd = (HWND)widget->GetNativeData(NS_NATIVE_WINDOW))) { return; } // Create invisible bitmap for caret, otherwise its appearance interferes // with Gecko caret HBITMAP caretBitMap = CreateBitmap(1, caretRect.height, 1, 1, nullptr); if (::CreateCaret(caretWnd, caretBitMap, 1, caretRect.height)) { // Also destroys the last caret ::ShowCaret(caretWnd); RECT windowRect; ::GetWindowRect(caretWnd, &windowRect); ::SetCaretPos(caretRect.x - windowRect.left, caretRect.y - windowRect.top); ::DeleteObject(caretBitMap); } } ITypeInfo* AccessibleWrap::GetTI(LCID lcid) { if (gTypeInfo) return gTypeInfo; ITypeLib *typeLib = nullptr; HRESULT hr = LoadRegTypeLib(LIBID_Accessibility, 1, 0, lcid, &typeLib); if (FAILED(hr)) return nullptr; hr = typeLib->GetTypeInfoOfGuid(IID_IAccessible, &gTypeInfo); typeLib->Release(); if (FAILED(hr)) return nullptr; return gTypeInfo; }