/* -*- 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/dom/TabChild.h" #include "mozilla/a11y/DocAccessibleChild.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 "nsIXULRuntime.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 MsaaIdGenerator AccessibleWrap::sIDGen; static const VARIANT kVarChildIdSelf = {VT_I4}; static const int32_t kIEnumVariantDisconnected = -1; //////////////////////////////////////////////////////////////////////////////// // AccessibleWrap //////////////////////////////////////////////////////////////////////////////// AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc) : Accessible(aContent, aDoc) , mID(kNoID) { } AccessibleWrap::~AccessibleWrap() { if (mID != kNoID) { sIDGen.ReleaseID(this); } } ITypeInfo* AccessibleWrap::gTypeInfo = nullptr; NS_IMPL_ISUPPORTS_INHERITED0(AccessibleWrap, Accessible) void AccessibleWrap::Shutdown() { if (mID != kNoID) { auto doc = static_cast(mDoc.get()); MOZ_ASSERT(doc); if (doc) { doc->RemoveID(mID); mID = kNoID; } } Accessible::Shutdown(); } //----------------------------------------------------- // IUnknown interface methods - see iunknown.h for documentation //----------------------------------------------------- // Microsoft COM QueryInterface STDMETHODIMP AccessibleWrap::QueryInterface(REFIID iid, void** ppv) { 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; } //----------------------------------------------------- // IAccessible methods //----------------------------------------------------- STDMETHODIMP AccessibleWrap::get_accParent( IDispatch __RPC_FAR *__RPC_FAR *ppdispParent) { if (!ppdispParent) return E_INVALIDARG; *ppdispParent = nullptr; if (IsDefunct()) return CO_E_OBJNOTCONNECTED; 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; } STDMETHODIMP AccessibleWrap::get_accChildCount( long __RPC_FAR *pcountChildren) { if (!pcountChildren) return E_INVALIDARG; *pcountChildren = 0; if (IsDefunct()) return CO_E_OBJNOTCONNECTED; if (nsAccUtils::MustPrune(this)) return S_OK; *pcountChildren = ChildCount(); return S_OK; } STDMETHODIMP AccessibleWrap::get_accChild( /* [in] */ VARIANT varChild, /* [retval][out] */ IDispatch __RPC_FAR *__RPC_FAR *ppdispChild) { 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. // The getting an accessible by child ID is used by AccessibleObjectFromEvent() // called by AT when AT handles our MSAA event. bool isDefunct = false; RefPtr child = GetIAccessibleFor(varChild, &isDefunct); if (!child) { return E_INVALIDARG; } if (isDefunct) { return CO_E_OBJNOTCONNECTED; } child.forget(ppdispChild); return S_OK; } /** * This function is a helper for implementing IAccessible methods that accept * a Child ID as a parameter. If the child ID is CHILDID_SELF, the function * returns S_OK but a null *aOutInterface. Otherwise, *aOutInterface points * to the resolved IAccessible. * * The CHILDID_SELF case is special because in that case we actually execute * the implementation of the IAccessible method, whereas in the non-self case, * we delegate the method call to that object for execution. * * A sample invocation of this would look like: * * RefPtr accessible; * HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); * if (FAILED(hr)) { * return hr; * } * * if (accessible) { * return accessible->get_accFoo(kVarChildIdSelf, pszName); * } * * // Implementation for CHILDID_SELF case goes here */ HRESULT AccessibleWrap::ResolveChild(const VARIANT& aVarChild, IAccessible** aOutInterface) { MOZ_ASSERT(aOutInterface); *aOutInterface = nullptr; if (aVarChild.vt != VT_I4) { return E_INVALIDARG; } if (IsDefunct()) { return CO_E_OBJNOTCONNECTED; } if (aVarChild.lVal == CHILDID_SELF) { return S_OK; } bool isDefunct = false; RefPtr accessible = GetIAccessibleFor(aVarChild, &isDefunct); if (!accessible) { return E_INVALIDARG; } if (isDefunct) { return CO_E_OBJNOTCONNECTED; } accessible.forget(aOutInterface); return S_OK; } STDMETHODIMP AccessibleWrap::get_accName( /* [optional][in] */ VARIANT varChild, /* [retval][out] */ BSTR __RPC_FAR *pszName) { if (!pszName || varChild.vt != VT_I4) return E_INVALIDARG; *pszName = nullptr; RefPtr accessible; HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); if (FAILED(hr)) { return hr; } if (accessible) { return accessible->get_accName(kVarChildIdSelf, pszName); } nsAutoString name; 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; } STDMETHODIMP AccessibleWrap::get_accValue( /* [optional][in] */ VARIANT varChild, /* [retval][out] */ BSTR __RPC_FAR *pszValue) { if (!pszValue) return E_INVALIDARG; *pszValue = nullptr; RefPtr accessible; HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); if (FAILED(hr)) { return hr; } if (accessible) { return accessible->get_accValue(kVarChildIdSelf, pszValue); } nsAutoString value; 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; } STDMETHODIMP AccessibleWrap::get_accDescription(VARIANT varChild, BSTR __RPC_FAR *pszDescription) { if (!pszDescription) return E_INVALIDARG; *pszDescription = nullptr; RefPtr accessible; HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); if (FAILED(hr)) { return hr; } if (accessible) { return accessible->get_accDescription(kVarChildIdSelf, pszDescription); } nsAutoString description; Description(description); *pszDescription = ::SysAllocStringLen(description.get(), description.Length()); return *pszDescription ? S_OK : E_OUTOFMEMORY; } STDMETHODIMP AccessibleWrap::get_accRole( /* [optional][in] */ VARIANT varChild, /* [retval][out] */ VARIANT __RPC_FAR *pvarRole) { if (!pvarRole) return E_INVALIDARG; VariantInit(pvarRole); RefPtr accessible; HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); if (FAILED(hr)) { return hr; } if (accessible) { return accessible->get_accRole(kVarChildIdSelf, pvarRole); } a11y::role geckoRole; #ifdef DEBUG NS_ASSERTION(nsAccUtils::IsTextInterfaceSupportCorrect(this), "Does not support Text when it should"); #endif geckoRole = 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 (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; } // -- 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 = 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->GetUncomposedDoc(); 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; } STDMETHODIMP AccessibleWrap::get_accState( /* [optional][in] */ VARIANT varChild, /* [retval][out] */ VARIANT __RPC_FAR *pvarState) { if (!pvarState) return E_INVALIDARG; VariantInit(pvarState); pvarState->vt = VT_I4; pvarState->lVal = 0; RefPtr accessible; HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); if (FAILED(hr)) { return hr; } if (accessible) { return accessible->get_accState(kVarChildIdSelf, pvarState); } // 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 = State(); uint32_t msaaState = 0; nsAccUtils::To32States(state, &msaaState, nullptr); pvarState->lVal = msaaState; return S_OK; } STDMETHODIMP AccessibleWrap::get_accHelp( /* [optional][in] */ VARIANT varChild, /* [retval][out] */ BSTR __RPC_FAR *pszHelp) { if (!pszHelp) return E_INVALIDARG; *pszHelp = nullptr; return S_FALSE; } STDMETHODIMP AccessibleWrap::get_accHelpTopic( /* [out] */ BSTR __RPC_FAR *pszHelpFile, /* [optional][in] */ VARIANT varChild, /* [retval][out] */ long __RPC_FAR *pidTopic) { if (!pszHelpFile || !pidTopic) return E_INVALIDARG; *pszHelpFile = nullptr; *pidTopic = 0; return S_FALSE; } STDMETHODIMP AccessibleWrap::get_accKeyboardShortcut( /* [optional][in] */ VARIANT varChild, /* [retval][out] */ BSTR __RPC_FAR *pszKeyboardShortcut) { if (!pszKeyboardShortcut) return E_INVALIDARG; *pszKeyboardShortcut = nullptr; RefPtr accessible; HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); if (FAILED(hr)) { return hr; } if (accessible) { return accessible->get_accKeyboardShortcut(kVarChildIdSelf, pszKeyboardShortcut); } KeyBinding keyBinding = AccessKey(); if (keyBinding.IsEmpty()) keyBinding = KeyboardShortcut(); nsAutoString shortcut; keyBinding.ToString(shortcut); *pszKeyboardShortcut = ::SysAllocStringLen(shortcut.get(), shortcut.Length()); return *pszKeyboardShortcut ? S_OK : E_OUTOFMEMORY; } STDMETHODIMP AccessibleWrap::get_accFocus( /* [retval][out] */ VARIANT __RPC_FAR *pvarChild) { 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 = 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; } /** * 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) { 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; } STDMETHODIMP AccessibleEnumerator::Next(unsigned long celt, VARIANT FAR* rgvar, unsigned long FAR* pceltFetched) { 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; } STDMETHODIMP AccessibleEnumerator::Clone(IEnumVARIANT FAR* FAR* ppenum) { *ppenum = new AccessibleEnumerator(*this); if (!*ppenum) return E_OUTOFMEMORY; NS_ADDREF(*ppenum); return S_OK; } STDMETHODIMP AccessibleEnumerator::Skip(unsigned long celt) { 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; } /** * 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) { if (!pvarChildren) return E_INVALIDARG; VariantInit(pvarChildren); pvarChildren->vt = VT_EMPTY; if (IsDefunct()) return CO_E_OBJNOTCONNECTED; if (IsSelect()) { AutoTArray selectedItems; 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; } STDMETHODIMP AccessibleWrap::get_accDefaultAction( /* [optional][in] */ VARIANT varChild, /* [retval][out] */ BSTR __RPC_FAR *pszDefaultAction) { if (!pszDefaultAction) return E_INVALIDARG; *pszDefaultAction = nullptr; RefPtr accessible; HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); if (FAILED(hr)) { return hr; } if (accessible) { return accessible->get_accDefaultAction(kVarChildIdSelf, pszDefaultAction); } nsAutoString defaultAction; ActionNameAt(0, defaultAction); *pszDefaultAction = ::SysAllocStringLen(defaultAction.get(), defaultAction.Length()); return *pszDefaultAction ? S_OK : E_OUTOFMEMORY; } STDMETHODIMP AccessibleWrap::accSelect( /* [in] */ long flagsSelect, /* [optional][in] */ VARIANT varChild) { RefPtr accessible; HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); if (FAILED(hr)) { return hr; } if (accessible) { return accessible->accSelect(flagsSelect, kVarChildIdSelf); } if (flagsSelect & SELFLAG_TAKEFOCUS) { if (XRE_IsContentProcess()) { // In this case we might have been invoked while the IPC MessageChannel is // waiting on a sync reply. We cannot dispatch additional IPC while that // is happening, so we dispatch TakeFocus from the main thread to // guarantee that we are outside any IPC. nsCOMPtr runnable = mozilla::NewRunnableMethod(this, &Accessible::TakeFocus); NS_DispatchToMainThread(runnable, NS_DISPATCH_NORMAL); return S_OK; } TakeFocus(); return S_OK; } if (flagsSelect & SELFLAG_TAKESELECTION) { TakeSelection(); return S_OK; } if (flagsSelect & SELFLAG_ADDSELECTION) { SetSelected(true); return S_OK; } if (flagsSelect & SELFLAG_REMOVESELECTION) { SetSelected(false); return S_OK; } return E_FAIL; } 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) { if (!pxLeft || !pyTop || !pcxWidth || !pcyHeight) return E_INVALIDARG; *pxLeft = 0; *pyTop = 0; *pcxWidth = 0; *pcyHeight = 0; RefPtr accessible; HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); if (FAILED(hr)) { return hr; } if (accessible) { return accessible->accLocation(pxLeft, pyTop, pcxWidth, pcyHeight, kVarChildIdSelf); } nsIntRect rect = Bounds(); *pxLeft = rect.x; *pyTop = rect.y; *pcxWidth = rect.width; *pcyHeight = rect.height; return S_OK; } STDMETHODIMP AccessibleWrap::accNavigate( /* [in] */ long navDir, /* [optional][in] */ VARIANT varStart, /* [retval][out] */ VARIANT __RPC_FAR *pvarEndUpAt) { if (!pvarEndUpAt) return E_INVALIDARG; VariantInit(pvarEndUpAt); RefPtr accessible; HRESULT hr = ResolveChild(varStart, getter_AddRefs(accessible)); if (FAILED(hr)) { return hr; } if (accessible) { return accessible->accNavigate(navDir, kVarChildIdSelf, pvarEndUpAt); } 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 (IsProxy()) { if (!Proxy()->MustPruneChildren()) { navAccessible = WrapperFor(Proxy()->FirstChild()); } } else { if (!nsAccUtils::MustPrune(this)) navAccessible = FirstChild(); } break; case NAVDIR_LASTCHILD: if (IsProxy()) { if (!Proxy()->MustPruneChildren()) { navAccessible = WrapperFor(Proxy()->LastChild()); } } else { if (!nsAccUtils::MustPrune(this)) navAccessible = LastChild(); } break; case NAVDIR_NEXT: navAccessible = IsProxy() ? WrapperFor(Proxy()->NextSibling()) : NextSibling(); break; case NAVDIR_PREVIOUS: navAccessible = IsProxy() ? WrapperFor(Proxy()->PrevSibling()) : 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) { Relation rel = RelationByType(*xpRelation); navAccessible = rel.Next(); } if (!navAccessible) return E_FAIL; pvarEndUpAt->pdispVal = NativeAccessible(navAccessible); pvarEndUpAt->vt = VT_DISPATCH; return S_OK; } STDMETHODIMP AccessibleWrap::accHitTest( /* [in] */ long xLeft, /* [in] */ long yTop, /* [retval][out] */ VARIANT __RPC_FAR *pvarChild) { if (!pvarChild) return E_INVALIDARG; VariantInit(pvarChild); if (IsDefunct()) return CO_E_OBJNOTCONNECTED; Accessible* 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; } STDMETHODIMP AccessibleWrap::accDoDefaultAction( /* [optional][in] */ VARIANT varChild) { RefPtr accessible; HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); if (FAILED(hr)) { return hr; } if (accessible) { return accessible->accDoDefaultAction(kVarChildIdSelf); } return DoAction(0) ? S_OK : E_INVALIDARG; } 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::SetID(uint32_t aID) { MOZ_ASSERT(XRE_IsParentProcess() && IsProxy()); mID = aID; } void AccessibleWrap::FireWinEvent(Accessible* aTarget, uint32_t aEventType) { MOZ_ASSERT(XRE_IsParentProcess()); 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; } // Chrome should use mID which has been generated by the content process. if (aAccessible->IsProxy()) { const uint32_t id = static_cast(aAccessible)->mID; MOZ_ASSERT(id != kNoID); return id; } if (!aAccessible->Document()) return 0; uint32_t* id = & static_cast(aAccessible)->mID; if (*id != kNoID) return *id; *id = sIDGen.GetID(); MOZ_ASSERT(!aAccessible->IsProxy()); DocAccessibleWrap* doc = static_cast(aAccessible->Document()); doc->AddID(*id, static_cast(aAccessible)); return *id; } HWND AccessibleWrap::GetHWNDFor(Accessible* aAccessible) { if (!aAccessible) { return nullptr; } if (XRE_IsContentProcess()) { DocAccessible* doc = aAccessible->Document(); if (!doc) { return nullptr; } DocAccessibleChild* ipcDoc = doc->IPCDoc(); if (!ipcDoc) { return nullptr; } auto tab = static_cast(ipcDoc->Manager()); MOZ_ASSERT(tab); return reinterpret_cast(tab->GetNativeWindowHandle()); } // 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); } 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; } static already_AddRefed GetProxiedAccessibleInSubtree(const DocAccessibleParent* aDoc, const VARIANT& aVarChild) { auto wrapper = static_cast(WrapperFor(aDoc)); RefPtr comProxy; int32_t wrapperChildId = AccessibleWrap::GetChildIDFor(wrapper); if (wrapperChildId == aVarChild.lVal) { wrapper->GetNativeInterface(getter_AddRefs(comProxy)); return comProxy.forget(); } MOZ_ASSERT(aDoc->IsTopLevel()); if (!aDoc->IsTopLevel()) { return nullptr; } wrapper->GetNativeInterface(getter_AddRefs(comProxy)); MOZ_ASSERT(comProxy); if (!comProxy) { return nullptr; } RefPtr disp; if (FAILED(comProxy->get_accChild(aVarChild, getter_AddRefs(disp)))) { return nullptr; } return disp.forget(); } already_AddRefed AccessibleWrap::GetIAccessibleFor(const VARIANT& aVarChild, bool* aIsDefunct) { if (aVarChild.vt != VT_I4) return nullptr; VARIANT varChild = aVarChild; MOZ_ASSERT(aIsDefunct); *aIsDefunct = false; RefPtr result; if (varChild.lVal == CHILDID_SELF) { *aIsDefunct = IsDefunct(); if (*aIsDefunct) { return nullptr; } GetNativeInterface(getter_AddRefs(result)); if (result) { return result.forget(); } // If we're not a proxy, there's nothing more we can do to attempt to // resolve the IAccessible, so we just fail. if (!IsProxy()) { return nullptr; } // Otherwise, since we're a proxy and we have a null native interface, this // indicates that we need to obtain a COM proxy. To do this, we'll replace // CHILDID_SELF with our real MSAA ID and continue the search from there. varChild.lVal = GetExistingID(); } if (IsProxy() ? Proxy()->MustPruneChildren() : nsAccUtils::MustPrune(this)) { return nullptr; } // If the MSAA ID is not a chrome id then we already know that we won't // find it here and should look remotely instead. if (XRE_IsParentProcess() && !sIDGen.IsChromeID(varChild.lVal)) { return GetRemoteIAccessibleFor(varChild); } MOZ_ASSERT(XRE_IsParentProcess() || sIDGen.IsIDForThisContentProcess(varChild.lVal)); if (varChild.lVal > 0) { // Gecko child indices are 0-based in contrast to indices used in MSAA. MOZ_ASSERT(!IsProxy()); Accessible* xpAcc = GetChildAt(varChild.lVal - 1); if (!xpAcc) { return nullptr; } *aIsDefunct = xpAcc->IsDefunct(); static_cast(xpAcc)->GetNativeInterface(getter_AddRefs(result)); return result.forget(); } // 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. // First handle the case that both this accessible and the id'd one are in // this process. if (!IsProxy()) { DocAccessible* document = Document(); Accessible* child = GetAccessibleInSubtree(document, static_cast(varChild.lVal)); // If it is a document then just return an accessible. if (child && IsDoc()) { *aIsDefunct = child->IsDefunct(); static_cast(child)->GetNativeInterface(getter_AddRefs(result)); return result.forget(); } // 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) { *aIsDefunct = child->IsDefunct(); static_cast(child)->GetNativeInterface(getter_AddRefs(result)); return result.forget(); } parent = parent->Parent(); } } // Now see about the case that both this accessible and the target one are // proxied. if (IsProxy()) { DocAccessibleParent* proxyDoc = Proxy()->Document(); RefPtr disp = GetProxiedAccessibleInSubtree(proxyDoc, varChild); if (!disp) { return nullptr; } MOZ_ASSERT(mscom::IsProxy(disp)); DebugOnly hr = disp->QueryInterface(IID_IAccessible, getter_AddRefs(result)); MOZ_ASSERT(SUCCEEDED(hr)); return result.forget(); } return nullptr; } already_AddRefed AccessibleWrap::GetRemoteIAccessibleFor(const VARIANT& aVarChild) { DocAccessibleParent* proxyDoc = nullptr; DocAccessible* doc = Document(); const nsTArray* remoteDocs = DocManager::TopLevelRemoteDocs(); if (!remoteDocs) { return nullptr; } RefPtr result; size_t docCount = remoteDocs->Length(); for (size_t i = 0; i < docCount; i++) { DocAccessibleParent* remoteDoc = remoteDocs->ElementAt(i); uint32_t remoteDocMsaaId = WrapperFor(remoteDoc)->GetExistingID(); if (!sIDGen.IsSameContentProcessFor(aVarChild.lVal, remoteDocMsaaId)) { continue; } Accessible* outerDoc = remoteDoc->OuterDocOfRemoteBrowser(); if (!outerDoc) { continue; } if (outerDoc->Document() != doc) { continue; } RefPtr disp = GetProxiedAccessibleInSubtree(remoteDoc, aVarChild); if (!disp) { continue; } DebugOnly hr = disp->QueryInterface(IID_IAccessible, getter_AddRefs(result)); MOZ_ASSERT(SUCCEEDED(hr)); return result.forget(); } 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; } /* static */ uint32_t AccessibleWrap::GetContentProcessIdFor(dom::ContentParentId aIPCContentId) { return sIDGen.GetContentProcessIDFor(aIPCContentId); } /* static */ void AccessibleWrap::ReleaseContentProcessIdFor(dom::ContentParentId aIPCContentId) { sIDGen.ReleaseContentProcessIDFor(aIPCContentId); }