/* -*- 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 "EnumVariant.h" #include "ia2AccessibleApplication.h" #include "ia2AccessibleHypertext.h" #include "ia2AccessibleImage.h" #include "ia2AccessibleTable.h" #include "ia2AccessibleTableCell.h" #include "LocalAccessible-inl.h" #include "mozilla/a11y/AccessibleWrap.h" #include "mozilla/a11y/Compatibility.h" #include "mozilla/a11y/DocAccessibleParent.h" #include "mozilla/StaticPrefs_accessibility.h" #include "MsaaAccessible.h" #include "MsaaDocAccessible.h" #include "MsaaRootAccessible.h" #include "MsaaXULMenuAccessible.h" #include "nsEventMap.h" #include "nsViewManager.h" #include "nsWinUtils.h" #include "Relation.h" #include "sdnAccessible.h" #include "sdnTextAccessible.h" #include "HyperTextAccessible-inl.h" #include "ServiceProvider.h" #include "Statistics.h" #include "ARIAMap.h" #include "mozilla/PresShell.h" using namespace mozilla; using namespace mozilla::a11y; static const VARIANT kVarChildIdSelf = {{{VT_I4}}}; // Used internally to safely get an MsaaAccessible from a COM pointer provided // to us by a client. static const GUID IID_MsaaAccessible = { /* a94aded3-1a9c-4afc-a32c-d6b5c010046b */ 0xa94aded3, 0x1a9c, 0x4afc, {0xa3, 0x2c, 0xd6, 0xb5, 0xc0, 0x10, 0x04, 0x6b}}; MsaaIdGenerator MsaaAccessible::sIDGen; ITypeInfo* MsaaAccessible::gTypeInfo = nullptr; /* static */ MsaaAccessible* MsaaAccessible::Create(Accessible* aAcc) { // This should only ever be called in the parent process. MOZ_ASSERT(XRE_IsParentProcess()); // The order of some of these is important! For example, when isRoot is true, // IsDoc will also be true, so we must check IsRoot first. IsTable/Cell and // IsHyperText are a similar case. if (aAcc->IsRoot()) { MOZ_ASSERT(aAcc->IsLocal()); return new MsaaRootAccessible(aAcc); } if (aAcc->IsDoc()) { return new MsaaDocAccessible(aAcc); } if (aAcc->IsTable()) { return new ia2AccessibleTable(aAcc); } if (aAcc->IsTableCell()) { return new ia2AccessibleTableCell(aAcc); } if (aAcc->IsApplication()) { MOZ_ASSERT(aAcc->IsLocal()); return new ia2AccessibleApplication(aAcc); } if (aAcc->IsImage()) { return new ia2AccessibleImage(aAcc); } if (LocalAccessible* localAcc = aAcc->AsLocal()) { if (localAcc->GetContent() && localAcc->GetContent()->IsXULElement(nsGkAtoms::menuitem)) { return new MsaaXULMenuitemAccessible(aAcc); } } if (aAcc->IsHyperText()) { return new ia2AccessibleHypertext(aAcc); } return new MsaaAccessible(aAcc); } MsaaAccessible::MsaaAccessible(Accessible* aAcc) : mAcc(aAcc), mID(kNoID) {} MsaaAccessible::~MsaaAccessible() { MOZ_ASSERT(!mAcc, "MsaaShutdown wasn't called!"); if (mID != kNoID) { sIDGen.ReleaseID(WrapNotNull(this)); } } void MsaaAccessible::MsaaShutdown() { // Accessibles can be shut down twice in some cases. If that happens, // MsaaShutdown will also be called twice because AccessibleWrap holds // the reference until its destructor is called; see the comments in // AccessibleWrap::Shutdown. if (!mAcc) { return; } if (mID != kNoID) { auto doc = MsaaDocAccessible::GetFromOwned(mAcc); MOZ_ASSERT(doc); doc->RemoveID(mID); } mAcc = nullptr; } int32_t MsaaAccessible::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; } auto doc = MsaaDocAccessible::GetFromOwned(aAccessible); if (!doc) { return 0; } uint32_t* id = &MsaaAccessible::GetFrom(aAccessible)->mID; if (*id != kNoID) return *id; *id = sIDGen.GetID(); doc->AddID(*id, aAccessible); return *id; } /* static */ void MsaaAccessible::AssignChildIDTo(NotNull aSdnAcc) { aSdnAcc->SetUniqueID(sIDGen.GetID()); } /* static */ void MsaaAccessible::ReleaseChildID(NotNull aSdnAcc) { sIDGen.ReleaseID(aSdnAcc); } HWND MsaaAccessible::GetHWNDFor(Accessible* aAccessible) { if (!aAccessible) { return nullptr; } LocalAccessible* localAcc = aAccessible->AsLocal(); if (!localAcc) { RemoteAccessible* proxy = aAccessible->AsRemote(); if (!proxy) { return nullptr; } // If window emulation is enabled, retrieve the emulated window from the // containing document document proxy. if (nsWinUtils::IsWindowEmulationStarted()) { DocAccessibleParent* doc = proxy->Document(); HWND hWnd = doc->GetEmulatedWindowHandle(); if (hWnd) { return hWnd; } } // 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. LocalAccessible* outerDoc = proxy->OuterDocOfRemoteBrowser(); if (!outerDoc) { // In some cases, the outer document accessible may be unattached from its // document at this point, if it is scheduled for removal. Do not assert // in such case. An example: putting aria-hidden="true" on HTML:iframe // element will destroy iframe's document asynchroniously, but // the document may be a target of selection events until then, and thus // it may attempt to deliever these events to MSAA clients. return nullptr; } return GetHWNDFor(outerDoc); } DocAccessible* document = localAcc->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 = localAcc->GetFrame(); if (frame) { nsIWidget* widget = frame->GetNearestWidget(); if (widget && widget->IsVisible()) { if (nsViewManager* vm = document->PresShellPtr()->GetViewManager()) { nsCOMPtr rootWidget = vm->GetRootWidget(); // 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()); } void MsaaAccessible::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"); if (aEventType == 0 || aEventType >= ArrayLength(gWinEventMap)) { MOZ_ASSERT_UNREACHABLE("invalid event type"); return; } uint32_t winEvent = gWinEventMap[aEventType]; if (!winEvent) return; int32_t childID = MsaaAccessible::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); } AccessibleWrap* MsaaAccessible::LocalAcc() { if (!mAcc || mAcc->IsRemote()) { return nullptr; } auto acc = static_cast(mAcc); MOZ_ASSERT(!acc || !acc->IsDefunct(), "mAcc defunct but MsaaShutdown wasn't called"); return acc; } /** * 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 MsaaAccessible::ResolveChild(const VARIANT& aVarChild, IAccessible** aOutInterface) { MOZ_ASSERT(aOutInterface); *aOutInterface = nullptr; if (aVarChild.vt != VT_I4) { return E_INVALIDARG; } if (!mAcc) { 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; } static Accessible* GetAccessibleInSubtree(DocAccessible* aDoc, uint32_t aID) { Accessible* child = MsaaDocAccessible::GetFrom(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 Accessible* GetAccessibleInSubtree(DocAccessibleParent* aDoc, uint32_t aID) { Accessible* child = MsaaDocAccessible::GetFrom(aDoc)->GetAccessibleByID(aID); if (child) { return child; } size_t childDocCount = aDoc->ChildDocCount(); for (size_t i = 0; i < childDocCount; i++) { child = GetAccessibleInSubtree(aDoc->ChildDocAt(i), aID); if (child) { return child; } } return nullptr; } static bool IsInclusiveDescendantOf(DocAccessible* aAncestor, DocAccessible* aDescendant) { for (DocAccessible* doc = aDescendant; doc; doc = doc->ParentDocument()) { if (doc == aAncestor) { return true; } } return false; } already_AddRefed MsaaAccessible::GetIAccessibleFor( const VARIANT& aVarChild, bool* aIsDefunct) { if (aVarChild.vt != VT_I4) return nullptr; VARIANT varChild = aVarChild; MOZ_ASSERT(aIsDefunct); *aIsDefunct = false; RefPtr result; if (!mAcc) { *aIsDefunct = true; return nullptr; } if (varChild.lVal == CHILDID_SELF) { result = this; return result.forget(); } if (varChild.ulVal != GetExistingID() && nsAccUtils::MustPrune(mAcc)) { // This accessible should have no subtree in platform, return null for its // children. return nullptr; } if (varChild.lVal > 0) { // Gecko child indices are 0-based in contrast to indices used in MSAA. Accessible* xpAcc = mAcc->ChildAt(varChild.lVal - 1); if (!xpAcc) { return nullptr; } MOZ_ASSERT(xpAcc->IsRemote() || !xpAcc->AsLocal()->IsDefunct(), "Shouldn't get a defunct child"); result = MsaaAccessible::GetFrom(xpAcc); 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. Accessible* doc = nullptr; Accessible* child = nullptr; auto id = static_cast(varChild.lVal); if (LocalAccessible* localAcc = mAcc->AsLocal()) { DocAccessible* localDoc = localAcc->Document(); doc = localDoc; child = GetAccessibleInSubtree(localDoc, id); if (!child) { // Search remote documents which are descendants of this local document. const auto remoteDocs = DocManager::TopLevelRemoteDocs(); if (!remoteDocs) { return nullptr; } for (DocAccessibleParent* remoteDoc : *remoteDocs) { LocalAccessible* outerDoc = remoteDoc->OuterDocOfRemoteBrowser(); if (!outerDoc || !IsInclusiveDescendantOf(localDoc, outerDoc->Document())) { continue; } child = GetAccessibleInSubtree(remoteDoc, id); if (child) { break; } } } } else { DocAccessibleParent* remoteDoc = mAcc->AsRemote()->Document(); doc = remoteDoc; child = GetAccessibleInSubtree(remoteDoc, id); } if (!child) { return nullptr; } MOZ_ASSERT(child->IsRemote() || !child->AsLocal()->IsDefunct(), "Shouldn't get a defunct child"); // If this method is being called on the document we searched, we can just // return child. if (mAcc == doc) { result = MsaaAccessible::GetFrom(child); return result.forget(); } // Otherwise, this method was called on a descendant, so we searched an // ancestor. We must check whether child is really a descendant. This is used // for ARIA documents and popups. Accessible* parent = child; while (parent && parent != doc) { if (parent == mAcc) { result = MsaaAccessible::GetFrom(child); return result.forget(); } parent = parent->Parent(); } return nullptr; } IDispatch* MsaaAccessible::NativeAccessible(Accessible* aAccessible) { if (!aAccessible) { NS_WARNING("Not passing in an aAccessible"); return nullptr; } RefPtr disp; disp = MsaaAccessible::GetFrom(aAccessible); IDispatch* rawDisp; disp.forget(&rawDisp); return rawDisp; } ITypeInfo* MsaaAccessible::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 */ MsaaAccessible* MsaaAccessible::GetFrom(Accessible* aAcc) { if (!aAcc) { return nullptr; } if (RemoteAccessible* remoteAcc = aAcc->AsRemote()) { return reinterpret_cast(remoteAcc->GetWrapper()); } return static_cast(aAcc)->GetMsaa(); } /* static */ Accessible* MsaaAccessible::GetAccessibleFrom(IUnknown* aUnknown) { RefPtr msaa; aUnknown->QueryInterface(IID_MsaaAccessible, getter_AddRefs(msaa)); if (!msaa) { return nullptr; } return msaa->Acc(); } // IUnknown methods STDMETHODIMP MsaaAccessible::QueryInterface(REFIID iid, void** ppv) { if (!ppv) return E_INVALIDARG; *ppv = nullptr; if (IID_IClientSecurity == iid) { // Some code might QI(IID_IClientSecurity) to detect whether or not we are // a proxy. Right now that can potentially happen off the main thread, so we // look for this condition immediately so that we don't trigger other code // that might not be thread-safe. return E_NOINTERFACE; } // These interfaces are always available. We can support querying to them // even if the Accessible is dead. if (IID_IUnknown == iid) { *ppv = static_cast(this); } else if (IID_MsaaAccessible == iid) { *ppv = static_cast(this); } else if (IID_IDispatch == iid || IID_IAccessible == iid) { *ppv = static_cast(this); } else if (IID_IServiceProvider == iid) { *ppv = new ServiceProvider(this); } else { HRESULT hr = ia2Accessible::QueryInterface(iid, ppv); if (SUCCEEDED(hr)) { return hr; } if (StaticPrefs::accessibility_uia_enable()) { hr = uiaRawElmProvider::QueryInterface(iid, ppv); if (SUCCEEDED(hr)) { return hr; } } } if (*ppv) { (reinterpret_cast(*ppv))->AddRef(); return S_OK; } // For interfaces below this point, we have to query the Accessible to // determine if they are available. if (!mAcc) { // mscom::Interceptor (and maybe other callers) expects either S_OK or // E_NOINTERFACE, so don't return CO_E_OBJNOTCONNECTED like we normally // would for a dead object. return E_NOINTERFACE; } AccessibleWrap* localAcc = LocalAcc(); if (IID_IEnumVARIANT == iid) { // We don't support this interface for leaf elements. if (!mAcc->HasChildren() || nsAccUtils::MustPrune(mAcc)) { return E_NOINTERFACE; } *ppv = static_cast(new ChildrenEnumVariant(this)); } else if (IID_ISimpleDOMNode == iid) { if (mAcc->IsDoc() || (localAcc && !localAcc->HasOwnContent())) { return E_NOINTERFACE; } *ppv = static_cast(new sdnAccessible(WrapNotNull(this))); } else if (iid == IID_ISimpleDOMText && localAcc && localAcc->IsTextLeaf()) { statistics::ISimpleDOMUsed(); *ppv = static_cast(new sdnTextAccessible(this)); static_cast(*ppv)->AddRef(); return S_OK; } if (!*ppv && localAcc) { HRESULT hr = ia2AccessibleComponent::QueryInterface(iid, ppv); if (SUCCEEDED(hr)) return hr; } if (!*ppv) { HRESULT hr = ia2AccessibleHyperlink::QueryInterface(iid, ppv); if (SUCCEEDED(hr)) return hr; } if (!*ppv) { 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 MsaaAccessible::get_accParent(IDispatch __RPC_FAR* __RPC_FAR* ppdispParent) { if (!ppdispParent) return E_INVALIDARG; *ppdispParent = nullptr; if (!mAcc) { return CO_E_OBJNOTCONNECTED; } Accessible* xpParentAcc = mAcc->Parent(); if (!xpParentAcc) return S_FALSE; *ppdispParent = NativeAccessible(xpParentAcc); return S_OK; } STDMETHODIMP MsaaAccessible::get_accChildCount(long __RPC_FAR* pcountChildren) { if (!pcountChildren) return E_INVALIDARG; *pcountChildren = 0; if (!mAcc) return CO_E_OBJNOTCONNECTED; if ((Compatibility::A11ySuppressionReasons() & SuppressionReasons::Clipboard) && mAcc->IsRoot()) { // Bug 1798098: Windows Suggested Actions (introduced in Windows 11 22H2) // might walk the entire a11y tree using UIA whenever anything is copied // to the clipboard. This causes an unacceptable hang, particularly when // the cache is disabled. We prevent this tree walk by returning a 0 child // count for the root window, from which Windows might walk. return S_OK; } if (nsAccUtils::MustPrune(mAcc)) { return S_OK; } *pcountChildren = mAcc->ChildCount(); return S_OK; } STDMETHODIMP MsaaAccessible::get_accChild( /* [in] */ VARIANT varChild, /* [retval][out] */ IDispatch __RPC_FAR* __RPC_FAR* ppdispChild) { if (!ppdispChild) return E_INVALIDARG; *ppdispChild = nullptr; if (!mAcc) 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; } STDMETHODIMP MsaaAccessible::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; Acc()->Name(name); if (name.IsVoid()) return S_FALSE; *pszName = ::SysAllocStringLen(name.get(), name.Length()); if (!*pszName) return E_OUTOFMEMORY; return S_OK; } STDMETHODIMP MsaaAccessible::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; Acc()->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 MsaaAccessible::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; Acc()->Description(description); *pszDescription = ::SysAllocStringLen(description.get(), description.Length()); return *pszDescription ? S_OK : E_OUTOFMEMORY; } STDMETHODIMP MsaaAccessible::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 if (mAcc->IsLocal()) { NS_ASSERTION(nsAccUtils::IsTextInterfaceSupportCorrect(mAcc->AsLocal()), "Does not support Text when it should"); } #endif geckoRole = mAcc->Role(); uint32_t msaaRole = 0; #define ROLE(_geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \ _msaaRole, ia2Role, androidClass, iosIsElement, uiaControlType, \ 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 = mAcc->Parent(); if (xpParent && xpParent->Role() == roles::TREE_TABLE) msaaRole = ROLE_SYSTEM_OUTLINEITEM; } pvarRole->vt = VT_I4; pvarRole->lVal = msaaRole; return S_OK; } STDMETHODIMP MsaaAccessible::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 = Acc()->State(); uint32_t msaaState = 0; nsAccUtils::To32States(state, &msaaState, nullptr); pvarState->lVal = msaaState; return S_OK; } STDMETHODIMP MsaaAccessible::get_accHelp( /* [optional][in] */ VARIANT varChild, /* [retval][out] */ BSTR __RPC_FAR* pszHelp) { if (!pszHelp) return E_INVALIDARG; *pszHelp = nullptr; return S_FALSE; } STDMETHODIMP MsaaAccessible::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 MsaaAccessible::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 = mAcc->AccessKey(); if (keyBinding.IsEmpty()) { if (LocalAccessible* localAcc = mAcc->AsLocal()) { keyBinding = localAcc->KeyboardShortcut(); } } nsAutoString shortcut; keyBinding.ToString(shortcut); *pszKeyboardShortcut = ::SysAllocStringLen(shortcut.get(), shortcut.Length()); return *pszKeyboardShortcut ? S_OK : E_OUTOFMEMORY; } STDMETHODIMP MsaaAccessible::get_accFocus( /* [retval][out] */ VARIANT __RPC_FAR* pvarChild) { if (!pvarChild) return E_INVALIDARG; VariantInit(pvarChild); // clang-format off // 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. // clang-format on if (!mAcc) { return CO_E_OBJNOTCONNECTED; } // Return the current IAccessible child that has focus Accessible* focusedAccessible = mAcc->FocusedChild(); if (focusedAccessible == mAcc) { 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: explicit AccessibleEnumerator(const nsTArray& aArray) : mArray(aArray.Clone()), mCurIndex(0) {} AccessibleEnumerator(const AccessibleEnumerator& toCopy) : mArray(toCopy.mArray.Clone()), 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 = MsaaAccessible::NativeAccessible(mArray[mCurIndex]); } if (pceltFetched) *pceltFetched = celt; return hr; } STDMETHODIMP AccessibleEnumerator::Clone(IEnumVARIANT FAR* FAR* ppenum) { *ppenum = new AccessibleEnumerator(*this); 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 MsaaAccessible::get_accSelection(VARIANT __RPC_FAR* pvarChildren) { if (!pvarChildren) return E_INVALIDARG; VariantInit(pvarChildren); pvarChildren->vt = VT_EMPTY; if (!mAcc) { return CO_E_OBJNOTCONNECTED; } Accessible* acc = Acc(); if (!acc->IsSelect()) { return S_OK; } AutoTArray selectedItems; acc->SelectedItems(&selectedItems); uint32_t count = selectedItems.Length(); if (count == 1) { pvarChildren->vt = VT_DISPATCH; pvarChildren->pdispVal = NativeAccessible(selectedItems[0]); } else if (count > 1) { RefPtr pEnum = new AccessibleEnumerator(selectedItems); pvarChildren->vt = VT_UNKNOWN; // this must be VT_UNKNOWN for an IEnumVARIANT NS_ADDREF(pvarChildren->punkVal = pEnum); } // If count == 0, vt is already VT_EMPTY, so there's nothing else to do. return S_OK; } STDMETHODIMP MsaaAccessible::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; mAcc->ActionNameAt(0, defaultAction); *pszDefaultAction = ::SysAllocStringLen(defaultAction.get(), defaultAction.Length()); return *pszDefaultAction ? S_OK : E_OUTOFMEMORY; } STDMETHODIMP MsaaAccessible::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) { mAcc->TakeFocus(); return S_OK; } if (flagsSelect & SELFLAG_TAKESELECTION) { mAcc->TakeSelection(); return S_OK; } if (flagsSelect & SELFLAG_ADDSELECTION) { mAcc->SetSelected(true); return S_OK; } if (flagsSelect & SELFLAG_REMOVESELECTION) { mAcc->SetSelected(false); return S_OK; } return E_FAIL; } STDMETHODIMP MsaaAccessible::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); } LayoutDeviceIntRect rect = Acc()->Bounds(); *pxLeft = rect.X(); *pyTop = rect.Y(); *pcxWidth = rect.Width(); *pcyHeight = rect.Height(); return S_OK; } STDMETHODIMP MsaaAccessible::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 (!nsAccUtils::MustPrune(mAcc)) { navAccessible = mAcc->FirstChild(); } break; case NAVDIR_LASTCHILD: if (!nsAccUtils::MustPrune(mAcc)) { navAccessible = mAcc->LastChild(); } break; case NAVDIR_NEXT: navAccessible = mAcc->NextSibling(); break; case NAVDIR_PREVIOUS: navAccessible = mAcc->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 = mAcc->RelationByType(*xpRelation); navAccessible = rel.Next(); } if (!navAccessible) return E_FAIL; pvarEndUpAt->pdispVal = NativeAccessible(navAccessible); pvarEndUpAt->vt = VT_DISPATCH; return S_OK; } STDMETHODIMP MsaaAccessible::accHitTest( /* [in] */ long xLeft, /* [in] */ long yTop, /* [retval][out] */ VARIANT __RPC_FAR* pvarChild) { if (!pvarChild) return E_INVALIDARG; VariantInit(pvarChild); if (!mAcc) { return CO_E_OBJNOTCONNECTED; } // The MSAA documentation says accHitTest should return a child. However, // clients call AccessibleObjectFromPoint, which ends up walking the // descendants calling accHitTest on each one. Since clients want the // deepest descendant anyway, it's faster and probably more accurate to // just do this ourselves. Accessible* accessible = mAcc->ChildAtPoint( xLeft, yTop, Accessible::EWhichChildAtPoint::DeepestChild); // if we got a child if (accessible) { if (accessible != mAcc && accessible->IsTextLeaf()) { Accessible* parent = accessible->Parent(); if (parent != mAcc && parent->Role() == roles::LINK) { // Bug 1843832: The UI Automation -> IAccessible2 proxy barfs if we // return the text leaf child of a link when hit testing an ancestor of // the link. Therefore, we return the link instead. MSAA clients which // call AccessibleObjectFromPoint will still get to the text leaf, since // AccessibleObjectFromPoint keeps calling accHitTest until it can't // descend any further. We should remove this tragic hack once we have // a native UIA implementation. accessible = parent; } } if (accessible == mAcc) { pvarChild->vt = VT_I4; pvarChild->lVal = CHILDID_SELF; } else { 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 MsaaAccessible::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 mAcc->DoAction(0) ? S_OK : E_INVALIDARG; } STDMETHODIMP MsaaAccessible::put_accName( /* [optional][in] */ VARIANT varChild, /* [in] */ BSTR szName) { return E_NOTIMPL; } STDMETHODIMP MsaaAccessible::put_accValue( /* [optional][in] */ VARIANT varChild, /* [in] */ BSTR szValue) { RefPtr accessible; HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); if (FAILED(hr)) { return hr; } if (accessible) { return accessible->put_accValue(kVarChildIdSelf, szValue); } HyperTextAccessibleBase* ht = mAcc->AsHyperTextBase(); if (!ht) { return E_NOTIMPL; } uint32_t length = ::SysStringLen(szValue); nsAutoString text(szValue, length); ht->ReplaceText(text); return S_OK; } // IDispatch methods STDMETHODIMP MsaaAccessible::GetTypeInfoCount(UINT* pctinfo) { if (!pctinfo) return E_INVALIDARG; *pctinfo = 1; return S_OK; } STDMETHODIMP MsaaAccessible::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 MsaaAccessible::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 MsaaAccessible::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); }