michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "Accessible-inl.h" michael@0: #include "AccIterator.h" michael@0: #include "DocAccessible-inl.h" michael@0: #include "HTMLImageMapAccessible.h" michael@0: #include "nsAccCache.h" michael@0: #include "nsAccessiblePivot.h" michael@0: #include "nsAccUtils.h" michael@0: #include "nsEventShell.h" michael@0: #include "nsTextEquivUtils.h" michael@0: #include "Role.h" michael@0: #include "RootAccessible.h" michael@0: #include "TreeWalker.h" michael@0: michael@0: #include "nsIMutableArray.h" michael@0: #include "nsICommandManager.h" michael@0: #include "nsIDocShell.h" michael@0: #include "nsIDocument.h" michael@0: #include "nsIDOMAttr.h" michael@0: #include "nsIDOMCharacterData.h" michael@0: #include "nsIDOMDocument.h" michael@0: #include "nsIDOMXULDocument.h" michael@0: #include "nsIDOMMutationEvent.h" michael@0: #include "nsPIDOMWindow.h" michael@0: #include "nsIDOMXULPopupElement.h" michael@0: #include "nsIEditingSession.h" michael@0: #include "nsIFrame.h" michael@0: #include "nsIInterfaceRequestorUtils.h" michael@0: #include "nsImageFrame.h" michael@0: #include "nsIPersistentProperties2.h" michael@0: #include "nsIPresShell.h" michael@0: #include "nsIServiceManager.h" michael@0: #include "nsViewManager.h" michael@0: #include "nsIScrollableFrame.h" michael@0: #include "nsUnicharUtils.h" michael@0: #include "nsIURI.h" michael@0: #include "nsIWebNavigation.h" michael@0: #include "nsFocusManager.h" michael@0: #include "nsNameSpaceManager.h" michael@0: #include "mozilla/ArrayUtils.h" michael@0: #include "mozilla/Assertions.h" michael@0: #include "mozilla/EventStates.h" michael@0: #include "mozilla/dom/DocumentType.h" michael@0: #include "mozilla/dom/Element.h" michael@0: michael@0: #ifdef MOZ_XUL michael@0: #include "nsIXULDocument.h" michael@0: #endif michael@0: michael@0: using namespace mozilla; michael@0: using namespace mozilla::a11y; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // Static member initialization michael@0: michael@0: static nsIAtom** kRelationAttrs[] = michael@0: { michael@0: &nsGkAtoms::aria_labelledby, michael@0: &nsGkAtoms::aria_describedby, michael@0: &nsGkAtoms::aria_owns, michael@0: &nsGkAtoms::aria_controls, michael@0: &nsGkAtoms::aria_flowto, michael@0: &nsGkAtoms::_for, michael@0: &nsGkAtoms::control michael@0: }; michael@0: michael@0: static const uint32_t kRelationAttrsLen = ArrayLength(kRelationAttrs); michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // Constructor/desctructor michael@0: michael@0: DocAccessible:: michael@0: DocAccessible(nsIDocument* aDocument, nsIContent* aRootContent, michael@0: nsIPresShell* aPresShell) : michael@0: HyperTextAccessibleWrap(aRootContent, this), michael@0: // XXX aaronl should we use an algorithm for the initial cache size? michael@0: mAccessibleCache(kDefaultCacheSize), michael@0: mNodeToAccessibleMap(kDefaultCacheSize), michael@0: mDocumentNode(aDocument), michael@0: mScrollPositionChangedTicks(0), michael@0: mLoadState(eTreeConstructionPending), mDocFlags(0), mLoadEventType(0), michael@0: mVirtualCursor(nullptr), michael@0: mPresShell(aPresShell) michael@0: { michael@0: mGenericTypes |= eDocument; michael@0: mStateFlags |= eNotNodeMapEntry; michael@0: michael@0: MOZ_ASSERT(mPresShell, "should have been given a pres shell"); michael@0: mPresShell->SetDocAccessible(this); michael@0: michael@0: // If this is a XUL Document, it should not implement nsHyperText michael@0: if (mDocumentNode && mDocumentNode->IsXUL()) michael@0: mGenericTypes &= ~eHyperText; michael@0: } michael@0: michael@0: DocAccessible::~DocAccessible() michael@0: { michael@0: NS_ASSERTION(!mPresShell, "LastRelease was never called!?!"); michael@0: } michael@0: michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // nsISupports michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_CLASS(DocAccessible) michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocAccessible, Accessible) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNotificationController) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVirtualCursor) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildDocuments) michael@0: tmp->mDependentIDsHash.EnumerateRead(CycleCollectorTraverseDepIDsEntry, &cb); michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAccessibleCache) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnchorJumpElm) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocAccessible, Accessible) michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK(mNotificationController) michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK(mVirtualCursor) michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK(mChildDocuments) michael@0: tmp->mDependentIDsHash.Clear(); michael@0: tmp->mNodeToAccessibleMap.Clear(); michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK(mAccessibleCache) michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK(mAnchorJumpElm) michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK_END michael@0: michael@0: NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(DocAccessible) michael@0: NS_INTERFACE_MAP_ENTRY(nsIAccessibleDocument) michael@0: NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver) michael@0: NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) michael@0: NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) michael@0: NS_INTERFACE_MAP_ENTRY(nsIObserver) michael@0: NS_INTERFACE_MAP_ENTRY(nsIAccessiblePivotObserver) michael@0: NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIAccessibleDocument) michael@0: foundInterface = 0; michael@0: michael@0: nsresult status; michael@0: if (!foundInterface) { michael@0: // HTML document accessible must inherit from HyperTextAccessible to get michael@0: // support text interfaces. XUL document accessible doesn't need this. michael@0: // However at some point we may push to implement the interfaces and michael@0: // return DocAccessible to inherit from AccessibleWrap. michael@0: michael@0: status = IsHyperText() ? michael@0: HyperTextAccessible::QueryInterface(aIID, (void**)&foundInterface) : michael@0: Accessible::QueryInterface(aIID, (void**)&foundInterface); michael@0: } else { michael@0: NS_ADDREF(foundInterface); michael@0: status = NS_OK; michael@0: } michael@0: michael@0: *aInstancePtr = foundInterface; michael@0: return status; michael@0: } michael@0: michael@0: NS_IMPL_ADDREF_INHERITED(DocAccessible, HyperTextAccessible) michael@0: NS_IMPL_RELEASE_INHERITED(DocAccessible, HyperTextAccessible) michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // nsIAccessible michael@0: michael@0: ENameValueFlag michael@0: DocAccessible::Name(nsString& aName) michael@0: { michael@0: aName.Truncate(); michael@0: michael@0: if (mParent) { michael@0: mParent->Name(aName); // Allow owning iframe to override the name michael@0: } michael@0: if (aName.IsEmpty()) { michael@0: // Allow name via aria-labelledby or title attribute michael@0: Accessible::Name(aName); michael@0: } michael@0: if (aName.IsEmpty()) { michael@0: GetTitle(aName); // Try title element michael@0: } michael@0: if (aName.IsEmpty()) { // Last resort: use URL michael@0: GetURL(aName); michael@0: } michael@0: michael@0: return eNameOK; michael@0: } michael@0: michael@0: // Accessible public method michael@0: role michael@0: DocAccessible::NativeRole() michael@0: { michael@0: nsCOMPtr docShell = nsCoreUtils::GetDocShellFor(mDocumentNode); michael@0: if (docShell) { michael@0: nsCOMPtr sameTypeRoot; michael@0: docShell->GetSameTypeRootTreeItem(getter_AddRefs(sameTypeRoot)); michael@0: int32_t itemType = docShell->ItemType(); michael@0: if (sameTypeRoot == docShell) { michael@0: // Root of content or chrome tree michael@0: if (itemType == nsIDocShellTreeItem::typeChrome) michael@0: return roles::CHROME_WINDOW; michael@0: michael@0: if (itemType == nsIDocShellTreeItem::typeContent) { michael@0: #ifdef MOZ_XUL michael@0: nsCOMPtr xulDoc(do_QueryInterface(mDocumentNode)); michael@0: if (xulDoc) michael@0: return roles::APPLICATION; michael@0: #endif michael@0: return roles::DOCUMENT; michael@0: } michael@0: } michael@0: else if (itemType == nsIDocShellTreeItem::typeContent) { michael@0: return roles::DOCUMENT; michael@0: } michael@0: } michael@0: michael@0: return roles::PANE; // Fall back; michael@0: } michael@0: michael@0: void michael@0: DocAccessible::Description(nsString& aDescription) michael@0: { michael@0: if (mParent) michael@0: mParent->Description(aDescription); michael@0: michael@0: if (HasOwnContent() && aDescription.IsEmpty()) { michael@0: nsTextEquivUtils:: michael@0: GetTextEquivFromIDRefs(this, nsGkAtoms::aria_describedby, michael@0: aDescription); michael@0: } michael@0: } michael@0: michael@0: // Accessible public method michael@0: uint64_t michael@0: DocAccessible::NativeState() michael@0: { michael@0: // Document is always focusable. michael@0: uint64_t state = states::FOCUSABLE; // keep in sync with NativeInteractiveState() impl michael@0: if (FocusMgr()->IsFocused(this)) michael@0: state |= states::FOCUSED; michael@0: michael@0: // Expose stale state until the document is ready (DOM is loaded and tree is michael@0: // constructed). michael@0: if (!HasLoadState(eReady)) michael@0: state |= states::STALE; michael@0: michael@0: // Expose state busy until the document and all its subdocuments is completely michael@0: // loaded. michael@0: if (!HasLoadState(eCompletelyLoaded)) michael@0: state |= states::BUSY; michael@0: michael@0: nsIFrame* frame = GetFrame(); michael@0: if (!frame || michael@0: !frame->IsVisibleConsideringAncestors(nsIFrame::VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY)) { michael@0: state |= states::INVISIBLE | states::OFFSCREEN; michael@0: } michael@0: michael@0: nsCOMPtr editor = GetEditor(); michael@0: state |= editor ? states::EDITABLE : states::READONLY; michael@0: michael@0: return state; michael@0: } michael@0: michael@0: uint64_t michael@0: DocAccessible::NativeInteractiveState() const michael@0: { michael@0: // Document is always focusable. michael@0: return states::FOCUSABLE; michael@0: } michael@0: michael@0: bool michael@0: DocAccessible::NativelyUnavailable() const michael@0: { michael@0: return false; michael@0: } michael@0: michael@0: // Accessible public method michael@0: void michael@0: DocAccessible::ApplyARIAState(uint64_t* aState) const michael@0: { michael@0: // Grab states from content element. michael@0: if (mContent) michael@0: Accessible::ApplyARIAState(aState); michael@0: michael@0: // Allow iframe/frame etc. to have final state override via ARIA. michael@0: if (mParent) michael@0: mParent->ApplyARIAState(aState); michael@0: } michael@0: michael@0: already_AddRefed michael@0: DocAccessible::Attributes() michael@0: { michael@0: nsCOMPtr attributes = michael@0: HyperTextAccessibleWrap::Attributes(); michael@0: michael@0: // No attributes if document is not attached to the tree or if it's a root michael@0: // document. michael@0: if (!mParent || IsRoot()) michael@0: return attributes.forget(); michael@0: michael@0: // Override ARIA object attributes from outerdoc. michael@0: aria::AttrIterator attribIter(mParent->GetContent()); michael@0: nsAutoString name, value, unused; michael@0: while(attribIter.Next(name, value)) michael@0: attributes->SetStringProperty(NS_ConvertUTF16toUTF8(name), value, unused); michael@0: michael@0: return attributes.forget(); michael@0: } michael@0: michael@0: Accessible* michael@0: DocAccessible::FocusedChild() michael@0: { michael@0: // Return an accessible for the current global focus, which does not have to michael@0: // be contained within the current document. michael@0: return FocusMgr()->FocusedAccessible(); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: DocAccessible::TakeFocus() michael@0: { michael@0: if (IsDefunct()) michael@0: return NS_ERROR_FAILURE; michael@0: michael@0: // Focus the document. michael@0: nsFocusManager* fm = nsFocusManager::GetFocusManager(); michael@0: NS_ENSURE_STATE(fm); michael@0: michael@0: nsCOMPtr newFocus; michael@0: return fm->MoveFocus(mDocumentNode->GetWindow(), nullptr, michael@0: nsIFocusManager::MOVEFOCUS_ROOT, 0, michael@0: getter_AddRefs(newFocus)); michael@0: } michael@0: michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // nsIAccessibleDocument michael@0: michael@0: NS_IMETHODIMP michael@0: DocAccessible::GetURL(nsAString& aURL) michael@0: { michael@0: if (IsDefunct()) michael@0: return NS_ERROR_FAILURE; michael@0: michael@0: nsCOMPtr container = mDocumentNode->GetContainer(); michael@0: nsCOMPtr webNav(do_GetInterface(container)); michael@0: nsAutoCString theURL; michael@0: if (webNav) { michael@0: nsCOMPtr pURI; michael@0: webNav->GetCurrentURI(getter_AddRefs(pURI)); michael@0: if (pURI) michael@0: pURI->GetSpec(theURL); michael@0: } michael@0: CopyUTF8toUTF16(theURL, aURL); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: DocAccessible::GetTitle(nsAString& aTitle) michael@0: { michael@0: if (!mDocumentNode) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: nsString title; michael@0: mDocumentNode->GetTitle(title); michael@0: aTitle = title; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: DocAccessible::GetMimeType(nsAString& aMimeType) michael@0: { michael@0: if (!mDocumentNode) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: return mDocumentNode->GetContentType(aMimeType); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: DocAccessible::GetDocType(nsAString& aDocType) michael@0: { michael@0: #ifdef MOZ_XUL michael@0: nsCOMPtr xulDoc(do_QueryInterface(mDocumentNode)); michael@0: if (xulDoc) { michael@0: aDocType.AssignLiteral("window"); // doctype not implemented for XUL at time of writing - causes assertion michael@0: return NS_OK; michael@0: } else michael@0: #endif michael@0: if (mDocumentNode) { michael@0: dom::DocumentType* docType = mDocumentNode->GetDoctype(); michael@0: if (docType) { michael@0: return docType->GetPublicId(aDocType); michael@0: } michael@0: } michael@0: michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: DocAccessible::GetNameSpaceURIForID(int16_t aNameSpaceID, nsAString& aNameSpaceURI) michael@0: { michael@0: if (mDocumentNode) { michael@0: nsNameSpaceManager* nameSpaceManager = nsNameSpaceManager::GetInstance(); michael@0: if (nameSpaceManager) michael@0: return nameSpaceManager->GetNameSpaceURI(aNameSpaceID, aNameSpaceURI); michael@0: } michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: DocAccessible::GetWindowHandle(void** aWindow) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aWindow); michael@0: *aWindow = GetNativeWindow(); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: DocAccessible::GetWindow(nsIDOMWindow** aDOMWin) michael@0: { michael@0: *aDOMWin = nullptr; michael@0: if (!mDocumentNode) { michael@0: return NS_ERROR_FAILURE; // Accessible is Shutdown() michael@0: } michael@0: *aDOMWin = mDocumentNode->GetWindow(); michael@0: michael@0: if (!*aDOMWin) michael@0: return NS_ERROR_FAILURE; // No DOM Window michael@0: michael@0: NS_ADDREF(*aDOMWin); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: DocAccessible::GetDOMDocument(nsIDOMDocument** aDOMDocument) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aDOMDocument); michael@0: *aDOMDocument = nullptr; michael@0: michael@0: if (mDocumentNode) michael@0: CallQueryInterface(mDocumentNode, aDOMDocument); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: DocAccessible::GetParentDocument(nsIAccessibleDocument** aDocument) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aDocument); michael@0: *aDocument = nullptr; michael@0: michael@0: if (!IsDefunct()) michael@0: NS_IF_ADDREF(*aDocument = ParentDocument()); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: DocAccessible::GetChildDocumentCount(uint32_t* aCount) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aCount); michael@0: *aCount = 0; michael@0: michael@0: if (!IsDefunct()) michael@0: *aCount = ChildDocumentCount(); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: DocAccessible::GetChildDocumentAt(uint32_t aIndex, michael@0: nsIAccessibleDocument** aDocument) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aDocument); michael@0: *aDocument = nullptr; michael@0: michael@0: if (IsDefunct()) michael@0: return NS_OK; michael@0: michael@0: NS_IF_ADDREF(*aDocument = GetChildDocumentAt(aIndex)); michael@0: return *aDocument ? NS_OK : NS_ERROR_INVALID_ARG; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: DocAccessible::GetVirtualCursor(nsIAccessiblePivot** aVirtualCursor) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aVirtualCursor); michael@0: *aVirtualCursor = nullptr; michael@0: michael@0: if (IsDefunct()) michael@0: return NS_ERROR_FAILURE; michael@0: michael@0: if (!mVirtualCursor) { michael@0: mVirtualCursor = new nsAccessiblePivot(this); michael@0: mVirtualCursor->AddObserver(this); michael@0: } michael@0: michael@0: NS_ADDREF(*aVirtualCursor = mVirtualCursor); michael@0: return NS_OK; michael@0: } michael@0: michael@0: // HyperTextAccessible method michael@0: already_AddRefed michael@0: DocAccessible::GetEditor() const michael@0: { michael@0: // Check if document is editable (designMode="on" case). Otherwise check if michael@0: // the html:body (for HTML document case) or document element is editable. michael@0: if (!mDocumentNode->HasFlag(NODE_IS_EDITABLE) && michael@0: (!mContent || !mContent->HasFlag(NODE_IS_EDITABLE))) michael@0: return nullptr; michael@0: michael@0: nsCOMPtr container = mDocumentNode->GetContainer(); michael@0: nsCOMPtr editingSession(do_GetInterface(container)); michael@0: if (!editingSession) michael@0: return nullptr; // No editing session interface michael@0: michael@0: nsCOMPtr editor; michael@0: editingSession->GetEditorForWindow(mDocumentNode->GetWindow(), getter_AddRefs(editor)); michael@0: if (!editor) michael@0: return nullptr; michael@0: michael@0: bool isEditable = false; michael@0: editor->GetIsDocumentEditable(&isEditable); michael@0: if (isEditable) michael@0: return editor.forget(); michael@0: michael@0: return nullptr; michael@0: } michael@0: michael@0: // DocAccessible public method michael@0: Accessible* michael@0: DocAccessible::GetAccessible(nsINode* aNode) const michael@0: { michael@0: Accessible* accessible = mNodeToAccessibleMap.Get(aNode); michael@0: michael@0: // No accessible in the cache, check if the given ID is unique ID of this michael@0: // document accessible. michael@0: if (!accessible) { michael@0: if (GetNode() != aNode) michael@0: return nullptr; michael@0: michael@0: accessible = const_cast(this); michael@0: } michael@0: michael@0: #ifdef DEBUG michael@0: // All cached accessible nodes should be in the parent michael@0: // It will assert if not all the children were created michael@0: // when they were first cached, and no invalidation michael@0: // ever corrected parent accessible's child cache. michael@0: Accessible* parent = accessible->Parent(); michael@0: if (parent) michael@0: parent->TestChildCache(accessible); michael@0: #endif michael@0: michael@0: return accessible; michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // Accessible michael@0: michael@0: void michael@0: DocAccessible::Init() michael@0: { michael@0: #ifdef A11Y_LOG michael@0: if (logging::IsEnabled(logging::eDocCreate)) michael@0: logging::DocCreate("document initialize", mDocumentNode, this); michael@0: #endif michael@0: michael@0: // Initialize notification controller. michael@0: mNotificationController = new NotificationController(this, mPresShell); michael@0: michael@0: // Mark the document accessible as loaded if its DOM document was loaded at michael@0: // this point (this can happen because a11y is started late or DOM document michael@0: // having no container was loaded. michael@0: if (mDocumentNode->GetReadyStateEnum() == nsIDocument::READYSTATE_COMPLETE) michael@0: mLoadState |= eDOMLoaded; michael@0: michael@0: AddEventListeners(); michael@0: } michael@0: michael@0: void michael@0: DocAccessible::Shutdown() michael@0: { michael@0: if (!mPresShell) // already shutdown michael@0: return; michael@0: michael@0: #ifdef A11Y_LOG michael@0: if (logging::IsEnabled(logging::eDocDestroy)) michael@0: logging::DocDestroy("document shutdown", mDocumentNode, this); michael@0: #endif michael@0: michael@0: if (mNotificationController) { michael@0: mNotificationController->Shutdown(); michael@0: mNotificationController = nullptr; michael@0: } michael@0: michael@0: RemoveEventListeners(); michael@0: michael@0: // Mark the document as shutdown before AT is notified about the document michael@0: // removal from its container (valid for root documents on ATK and due to michael@0: // some reason for MSAA, refer to bug 757392 for details). michael@0: mStateFlags |= eIsDefunct; michael@0: nsCOMPtr kungFuDeathGripDoc = mDocumentNode; michael@0: mDocumentNode = nullptr; michael@0: michael@0: if (mParent) { michael@0: DocAccessible* parentDocument = mParent->Document(); michael@0: if (parentDocument) michael@0: parentDocument->RemoveChildDocument(this); michael@0: michael@0: mParent->RemoveChild(this); michael@0: } michael@0: michael@0: // Walk the array backwards because child documents remove themselves from the michael@0: // array as they are shutdown. michael@0: int32_t childDocCount = mChildDocuments.Length(); michael@0: for (int32_t idx = childDocCount - 1; idx >= 0; idx--) michael@0: mChildDocuments[idx]->Shutdown(); michael@0: michael@0: mChildDocuments.Clear(); michael@0: michael@0: if (mVirtualCursor) { michael@0: mVirtualCursor->RemoveObserver(this); michael@0: mVirtualCursor = nullptr; michael@0: } michael@0: michael@0: mPresShell->SetDocAccessible(nullptr); michael@0: mPresShell = nullptr; // Avoid reentrancy michael@0: michael@0: mDependentIDsHash.Clear(); michael@0: mNodeToAccessibleMap.Clear(); michael@0: ClearCache(mAccessibleCache); michael@0: michael@0: HyperTextAccessibleWrap::Shutdown(); michael@0: michael@0: GetAccService()->NotifyOfDocumentShutdown(kungFuDeathGripDoc); michael@0: } michael@0: michael@0: nsIFrame* michael@0: DocAccessible::GetFrame() const michael@0: { michael@0: nsIFrame* root = nullptr; michael@0: if (mPresShell) michael@0: root = mPresShell->GetRootFrame(); michael@0: michael@0: return root; michael@0: } michael@0: michael@0: // DocAccessible protected member michael@0: void michael@0: DocAccessible::GetBoundsRect(nsRect& aBounds, nsIFrame** aRelativeFrame) michael@0: { michael@0: *aRelativeFrame = GetFrame(); michael@0: michael@0: nsIDocument *document = mDocumentNode; michael@0: nsIDocument *parentDoc = nullptr; michael@0: michael@0: while (document) { michael@0: nsIPresShell *presShell = document->GetShell(); michael@0: if (!presShell) { michael@0: return; michael@0: } michael@0: michael@0: nsRect scrollPort; michael@0: nsIScrollableFrame* sf = presShell->GetRootScrollFrameAsScrollableExternal(); michael@0: if (sf) { michael@0: scrollPort = sf->GetScrollPortRect(); michael@0: } else { michael@0: nsIFrame* rootFrame = presShell->GetRootFrame(); michael@0: if (!rootFrame) { michael@0: return; michael@0: } michael@0: scrollPort = rootFrame->GetRect(); michael@0: } michael@0: michael@0: if (parentDoc) { // After first time thru loop michael@0: // XXXroc bogus code! scrollPort is relative to the viewport of michael@0: // this document, but we're intersecting rectangles derived from michael@0: // multiple documents and assuming they're all in the same coordinate michael@0: // system. See bug 514117. michael@0: aBounds.IntersectRect(scrollPort, aBounds); michael@0: } michael@0: else { // First time through loop michael@0: aBounds = scrollPort; michael@0: } michael@0: michael@0: document = parentDoc = document->GetParentDocument(); michael@0: } michael@0: } michael@0: michael@0: // DocAccessible protected member michael@0: nsresult michael@0: DocAccessible::AddEventListeners() michael@0: { michael@0: nsCOMPtr docShellTreeItem(mDocumentNode->GetDocShell()); michael@0: michael@0: // We want to add a command observer only if the document is content and has michael@0: // an editor. michael@0: if (docShellTreeItem->ItemType() == nsIDocShellTreeItem::typeContent) { michael@0: nsCOMPtr commandManager = do_GetInterface(docShellTreeItem); michael@0: if (commandManager) michael@0: commandManager->AddCommandObserver(this, "obs_documentCreated"); michael@0: } michael@0: michael@0: SelectionMgr()->AddDocSelectionListener(mPresShell); michael@0: michael@0: // Add document observer. michael@0: mDocumentNode->AddObserver(this); michael@0: return NS_OK; michael@0: } michael@0: michael@0: // DocAccessible protected member michael@0: nsresult michael@0: DocAccessible::RemoveEventListeners() michael@0: { michael@0: // Remove listeners associated with content documents michael@0: // Remove scroll position listener michael@0: RemoveScrollListener(); michael@0: michael@0: NS_ASSERTION(mDocumentNode, "No document during removal of listeners."); michael@0: michael@0: if (mDocumentNode) { michael@0: mDocumentNode->RemoveObserver(this); michael@0: michael@0: nsCOMPtr docShellTreeItem(mDocumentNode->GetDocShell()); michael@0: NS_ASSERTION(docShellTreeItem, "doc should support nsIDocShellTreeItem."); michael@0: michael@0: if (docShellTreeItem) { michael@0: if (docShellTreeItem->ItemType() == nsIDocShellTreeItem::typeContent) { michael@0: nsCOMPtr commandManager = do_GetInterface(docShellTreeItem); michael@0: if (commandManager) { michael@0: commandManager->RemoveCommandObserver(this, "obs_documentCreated"); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (mScrollWatchTimer) { michael@0: mScrollWatchTimer->Cancel(); michael@0: mScrollWatchTimer = nullptr; michael@0: NS_RELEASE_THIS(); // Kung fu death grip michael@0: } michael@0: michael@0: SelectionMgr()->RemoveDocSelectionListener(mPresShell); michael@0: return NS_OK; michael@0: } michael@0: michael@0: void michael@0: DocAccessible::ScrollTimerCallback(nsITimer* aTimer, void* aClosure) michael@0: { michael@0: DocAccessible* docAcc = reinterpret_cast(aClosure); michael@0: michael@0: if (docAcc && docAcc->mScrollPositionChangedTicks && michael@0: ++docAcc->mScrollPositionChangedTicks > 2) { michael@0: // Whenever scroll position changes, mScrollPositionChangeTicks gets reset to 1 michael@0: // We only want to fire accessibilty scroll event when scrolling stops or pauses michael@0: // Therefore, we wait for no scroll events to occur between 2 ticks of this timer michael@0: // That indicates a pause in scrolling, so we fire the accessibilty scroll event michael@0: nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_SCROLLING_END, docAcc); michael@0: michael@0: docAcc->mScrollPositionChangedTicks = 0; michael@0: if (docAcc->mScrollWatchTimer) { michael@0: docAcc->mScrollWatchTimer->Cancel(); michael@0: docAcc->mScrollWatchTimer = nullptr; michael@0: NS_RELEASE(docAcc); // Release kung fu death grip michael@0: } michael@0: } michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // nsIScrollPositionListener michael@0: michael@0: void michael@0: DocAccessible::ScrollPositionDidChange(nscoord aX, nscoord aY) michael@0: { michael@0: // Start new timer, if the timer cycles at least 1 full cycle without more scroll position changes, michael@0: // then the ::Notify() method will fire the accessibility event for scroll position changes michael@0: const uint32_t kScrollPosCheckWait = 50; michael@0: if (mScrollWatchTimer) { michael@0: mScrollWatchTimer->SetDelay(kScrollPosCheckWait); // Create new timer, to avoid leaks michael@0: } michael@0: else { michael@0: mScrollWatchTimer = do_CreateInstance("@mozilla.org/timer;1"); michael@0: if (mScrollWatchTimer) { michael@0: NS_ADDREF_THIS(); // Kung fu death grip michael@0: mScrollWatchTimer->InitWithFuncCallback(ScrollTimerCallback, this, michael@0: kScrollPosCheckWait, michael@0: nsITimer::TYPE_REPEATING_SLACK); michael@0: } michael@0: } michael@0: mScrollPositionChangedTicks = 1; michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // nsIObserver michael@0: michael@0: NS_IMETHODIMP michael@0: DocAccessible::Observe(nsISupports* aSubject, const char* aTopic, michael@0: const char16_t* aData) michael@0: { michael@0: if (!nsCRT::strcmp(aTopic,"obs_documentCreated")) { michael@0: // State editable will now be set, readonly is now clear michael@0: // Normally we only fire delayed events created from the node, not an michael@0: // accessible object. See the AccStateChangeEvent constructor for details michael@0: // about this exceptional case. michael@0: nsRefPtr event = michael@0: new AccStateChangeEvent(this, states::EDITABLE, true); michael@0: FireDelayedEvent(event); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // nsIAccessiblePivotObserver michael@0: michael@0: NS_IMETHODIMP michael@0: DocAccessible::OnPivotChanged(nsIAccessiblePivot* aPivot, michael@0: nsIAccessible* aOldAccessible, michael@0: int32_t aOldStart, int32_t aOldEnd, michael@0: PivotMoveReason aReason) michael@0: { michael@0: nsRefPtr event = new AccVCChangeEvent(this, aOldAccessible, michael@0: aOldStart, aOldEnd, michael@0: aReason); michael@0: nsEventShell::FireEvent(event); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // nsIDocumentObserver michael@0: michael@0: NS_IMPL_NSIDOCUMENTOBSERVER_CORE_STUB(DocAccessible) michael@0: NS_IMPL_NSIDOCUMENTOBSERVER_LOAD_STUB(DocAccessible) michael@0: NS_IMPL_NSIDOCUMENTOBSERVER_STYLE_STUB(DocAccessible) michael@0: michael@0: void michael@0: DocAccessible::AttributeWillChange(nsIDocument* aDocument, michael@0: dom::Element* aElement, michael@0: int32_t aNameSpaceID, michael@0: nsIAtom* aAttribute, int32_t aModType) michael@0: { michael@0: Accessible* accessible = GetAccessible(aElement); michael@0: if (!accessible) { michael@0: if (aElement != mContent) michael@0: return; michael@0: michael@0: accessible = this; michael@0: } michael@0: michael@0: // Update dependent IDs cache. Take care of elements that are accessible michael@0: // because dependent IDs cache doesn't contain IDs from non accessible michael@0: // elements. michael@0: if (aModType != nsIDOMMutationEvent::ADDITION) michael@0: RemoveDependentIDsFor(aElement, aAttribute); michael@0: michael@0: // Store the ARIA attribute old value so that it can be used after michael@0: // attribute change. Note, we assume there's no nested ARIA attribute michael@0: // changes. If this happens then we should end up with keeping a stack of michael@0: // old values. michael@0: michael@0: // XXX TODO: bugs 472142, 472143. michael@0: // Here we will want to cache whatever attribute values we are interested michael@0: // in, such as the existence of aria-pressed for button (so we know if we michael@0: // need to newly expose it as a toggle button) etc. michael@0: if (aAttribute == nsGkAtoms::aria_checked || michael@0: aAttribute == nsGkAtoms::aria_pressed) { michael@0: mARIAAttrOldValue = (aModType != nsIDOMMutationEvent::ADDITION) ? michael@0: nsAccUtils::GetARIAToken(aElement, aAttribute) : nullptr; michael@0: return; michael@0: } michael@0: michael@0: if (aAttribute == nsGkAtoms::aria_disabled || michael@0: aAttribute == nsGkAtoms::disabled) michael@0: mStateBitWasOn = accessible->Unavailable(); michael@0: } michael@0: michael@0: void michael@0: DocAccessible::AttributeChanged(nsIDocument* aDocument, michael@0: dom::Element* aElement, michael@0: int32_t aNameSpaceID, nsIAtom* aAttribute, michael@0: int32_t aModType) michael@0: { michael@0: NS_ASSERTION(!IsDefunct(), michael@0: "Attribute changed called on defunct document accessible!"); michael@0: michael@0: // Proceed even if the element is not accessible because element may become michael@0: // accessible if it gets certain attribute. michael@0: if (UpdateAccessibleOnAttrChange(aElement, aAttribute)) michael@0: return; michael@0: michael@0: // Ignore attribute change if the element doesn't have an accessible (at all michael@0: // or still) iff the element is not a root content of this document accessible michael@0: // (which is treated as attribute change on this document accessible). michael@0: // Note: we don't bail if all the content hasn't finished loading because michael@0: // these attributes are changing for a loaded part of the content. michael@0: Accessible* accessible = GetAccessible(aElement); michael@0: if (!accessible) { michael@0: if (mContent != aElement) michael@0: return; michael@0: michael@0: accessible = this; michael@0: } michael@0: michael@0: // Fire accessible events iff there's an accessible, otherwise we consider michael@0: // the accessible state wasn't changed, i.e. its state is initial state. michael@0: AttributeChangedImpl(accessible, aNameSpaceID, aAttribute); michael@0: michael@0: // Update dependent IDs cache. Take care of accessible elements because no michael@0: // accessible element means either the element is not accessible at all or michael@0: // its accessible will be created later. It doesn't make sense to keep michael@0: // dependent IDs for non accessible elements. For the second case we'll update michael@0: // dependent IDs cache when its accessible is created. michael@0: if (aModType == nsIDOMMutationEvent::MODIFICATION || michael@0: aModType == nsIDOMMutationEvent::ADDITION) { michael@0: AddDependentIDsFor(aElement, aAttribute); michael@0: } michael@0: } michael@0: michael@0: // DocAccessible protected member michael@0: void michael@0: DocAccessible::AttributeChangedImpl(Accessible* aAccessible, michael@0: int32_t aNameSpaceID, nsIAtom* aAttribute) michael@0: { michael@0: // Fire accessible event after short timer, because we need to wait for michael@0: // DOM attribute & resulting layout to actually change. Otherwise, michael@0: // assistive technology will retrieve the wrong state/value/selection info. michael@0: michael@0: // XXX todo michael@0: // We still need to handle special HTML cases here michael@0: // For example, if an 's usemap attribute is modified michael@0: // Otherwise it may just be a state change, for example an object changing michael@0: // its visibility michael@0: // michael@0: // XXX todo: report aria state changes for "undefined" literal value changes michael@0: // filed as bug 472142 michael@0: // michael@0: // XXX todo: invalidate accessible when aria state changes affect exposed role michael@0: // filed as bug 472143 michael@0: michael@0: // Universal boolean properties that don't require a role. Fire the state michael@0: // change when disabled or aria-disabled attribute is set. michael@0: // Note. Checking the XUL or HTML namespace would not seem to gain us michael@0: // anything, because disabled attribute really is going to mean the same michael@0: // thing in any namespace. michael@0: // Note. We use the attribute instead of the disabled state bit because michael@0: // ARIA's aria-disabled does not affect the disabled state bit. michael@0: if (aAttribute == nsGkAtoms::disabled || michael@0: aAttribute == nsGkAtoms::aria_disabled) { michael@0: // Do nothing if state wasn't changed (like @aria-disabled was removed but michael@0: // @disabled is still presented). michael@0: if (aAccessible->Unavailable() == mStateBitWasOn) michael@0: return; michael@0: michael@0: nsRefPtr enabledChangeEvent = michael@0: new AccStateChangeEvent(aAccessible, states::ENABLED, mStateBitWasOn); michael@0: FireDelayedEvent(enabledChangeEvent); michael@0: michael@0: nsRefPtr sensitiveChangeEvent = michael@0: new AccStateChangeEvent(aAccessible, states::SENSITIVE, mStateBitWasOn); michael@0: FireDelayedEvent(sensitiveChangeEvent); michael@0: return; michael@0: } michael@0: michael@0: // Check for namespaced ARIA attribute michael@0: if (aNameSpaceID == kNameSpaceID_None) { michael@0: // Check for hyphenated aria-foo property? michael@0: if (StringBeginsWith(nsDependentAtomString(aAttribute), michael@0: NS_LITERAL_STRING("aria-"))) { michael@0: ARIAAttributeChanged(aAccessible, aAttribute); michael@0: } michael@0: } michael@0: michael@0: // Fire name change and description change events. XXX: it's not complete and michael@0: // dupes the code logic of accessible name and description calculation, we do michael@0: // that for performance reasons. michael@0: if (aAttribute == nsGkAtoms::aria_label) { michael@0: FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, aAccessible); michael@0: return; michael@0: } michael@0: michael@0: if (aAttribute == nsGkAtoms::aria_describedby) { michael@0: FireDelayedEvent(nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE, aAccessible); michael@0: return; michael@0: } michael@0: michael@0: nsIContent* elm = aAccessible->GetContent(); michael@0: if (aAttribute == nsGkAtoms::aria_labelledby && michael@0: !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_label)) { michael@0: FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, aAccessible); michael@0: return; michael@0: } michael@0: michael@0: if (aAttribute == nsGkAtoms::alt && michael@0: !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_label) && michael@0: !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_labelledby)) { michael@0: FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, aAccessible); michael@0: return; michael@0: } michael@0: michael@0: if (aAttribute == nsGkAtoms::title) { michael@0: if (!elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_label) && michael@0: !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_labelledby) && michael@0: !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::alt)) { michael@0: FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, aAccessible); michael@0: return; michael@0: } michael@0: michael@0: if (!elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_describedby)) michael@0: FireDelayedEvent(nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE, aAccessible); michael@0: michael@0: return; michael@0: } michael@0: michael@0: if (aAttribute == nsGkAtoms::aria_busy) { michael@0: bool isOn = elm->AttrValueIs(aNameSpaceID, aAttribute, nsGkAtoms::_true, michael@0: eCaseMatters); michael@0: nsRefPtr event = michael@0: new AccStateChangeEvent(aAccessible, states::BUSY, isOn); michael@0: FireDelayedEvent(event); michael@0: return; michael@0: } michael@0: michael@0: // ARIA or XUL selection michael@0: if ((aAccessible->GetContent()->IsXUL() && aAttribute == nsGkAtoms::selected) || michael@0: aAttribute == nsGkAtoms::aria_selected) { michael@0: Accessible* widget = michael@0: nsAccUtils::GetSelectableContainer(aAccessible, aAccessible->State()); michael@0: if (widget) { michael@0: AccSelChangeEvent::SelChangeType selChangeType = michael@0: elm->AttrValueIs(aNameSpaceID, aAttribute, nsGkAtoms::_true, eCaseMatters) ? michael@0: AccSelChangeEvent::eSelectionAdd : AccSelChangeEvent::eSelectionRemove; michael@0: michael@0: nsRefPtr event = michael@0: new AccSelChangeEvent(widget, aAccessible, selChangeType); michael@0: FireDelayedEvent(event); michael@0: } michael@0: michael@0: return; michael@0: } michael@0: michael@0: if (aAttribute == nsGkAtoms::contenteditable) { michael@0: nsRefPtr editableChangeEvent = michael@0: new AccStateChangeEvent(aAccessible, states::EDITABLE); michael@0: FireDelayedEvent(editableChangeEvent); michael@0: return; michael@0: } michael@0: michael@0: if (aAttribute == nsGkAtoms::value) { michael@0: if (aAccessible->IsProgress()) michael@0: FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, aAccessible); michael@0: } michael@0: } michael@0: michael@0: // DocAccessible protected member michael@0: void michael@0: DocAccessible::ARIAAttributeChanged(Accessible* aAccessible, nsIAtom* aAttribute) michael@0: { michael@0: // Note: For universal/global ARIA states and properties we don't care if michael@0: // there is an ARIA role present or not. michael@0: michael@0: if (aAttribute == nsGkAtoms::aria_required) { michael@0: nsRefPtr event = michael@0: new AccStateChangeEvent(aAccessible, states::REQUIRED); michael@0: FireDelayedEvent(event); michael@0: return; michael@0: } michael@0: michael@0: if (aAttribute == nsGkAtoms::aria_invalid) { michael@0: nsRefPtr event = michael@0: new AccStateChangeEvent(aAccessible, states::INVALID); michael@0: FireDelayedEvent(event); michael@0: return; michael@0: } michael@0: michael@0: // The activedescendant universal property redirects accessible focus events michael@0: // to the element with the id that activedescendant points to. Make sure michael@0: // the tree up to date before processing. michael@0: if (aAttribute == nsGkAtoms::aria_activedescendant) { michael@0: mNotificationController->HandleNotification michael@0: (this, &DocAccessible::ARIAActiveDescendantChanged, aAccessible); michael@0: michael@0: return; michael@0: } michael@0: michael@0: // We treat aria-expanded as a global ARIA state for historical reasons michael@0: if (aAttribute == nsGkAtoms::aria_expanded) { michael@0: nsRefPtr event = michael@0: new AccStateChangeEvent(aAccessible, states::EXPANDED); michael@0: FireDelayedEvent(event); michael@0: return; michael@0: } michael@0: michael@0: // For aria attributes like drag and drop changes we fire a generic attribute michael@0: // change event; at least until native API comes up with a more meaningful event. michael@0: uint8_t attrFlags = aria::AttrCharacteristicsFor(aAttribute); michael@0: if (!(attrFlags & ATTR_BYPASSOBJ)) michael@0: FireDelayedEvent(nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED, michael@0: aAccessible); michael@0: michael@0: nsIContent* elm = aAccessible->GetContent(); michael@0: michael@0: if (aAttribute == nsGkAtoms::aria_checked || michael@0: (aAccessible->IsButton() && michael@0: aAttribute == nsGkAtoms::aria_pressed)) { michael@0: const uint64_t kState = (aAttribute == nsGkAtoms::aria_checked) ? michael@0: states::CHECKED : states::PRESSED; michael@0: nsRefPtr event = new AccStateChangeEvent(aAccessible, kState); michael@0: FireDelayedEvent(event); michael@0: michael@0: bool wasMixed = (mARIAAttrOldValue == nsGkAtoms::mixed); michael@0: bool isMixed = elm->AttrValueIs(kNameSpaceID_None, aAttribute, michael@0: nsGkAtoms::mixed, eCaseMatters); michael@0: if (isMixed != wasMixed) { michael@0: nsRefPtr event = michael@0: new AccStateChangeEvent(aAccessible, states::MIXED, isMixed); michael@0: FireDelayedEvent(event); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: if (aAttribute == nsGkAtoms::aria_readonly) { michael@0: nsRefPtr event = michael@0: new AccStateChangeEvent(aAccessible, states::READONLY); michael@0: FireDelayedEvent(event); michael@0: return; michael@0: } michael@0: michael@0: // Fire value change event whenever aria-valuetext is changed, or michael@0: // when aria-valuenow is changed and aria-valuetext is empty michael@0: if (aAttribute == nsGkAtoms::aria_valuetext || michael@0: (aAttribute == nsGkAtoms::aria_valuenow && michael@0: (!elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_valuetext) || michael@0: elm->AttrValueIs(kNameSpaceID_None, nsGkAtoms::aria_valuetext, michael@0: nsGkAtoms::_empty, eCaseMatters)))) { michael@0: FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, aAccessible); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: void michael@0: DocAccessible::ARIAActiveDescendantChanged(Accessible* aAccessible) michael@0: { michael@0: nsIContent* elm = aAccessible->GetContent(); michael@0: if (elm && aAccessible->IsActiveWidget()) { michael@0: nsAutoString id; michael@0: if (elm->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_activedescendant, id)) { michael@0: dom::Element* activeDescendantElm = elm->OwnerDoc()->GetElementById(id); michael@0: if (activeDescendantElm) { michael@0: Accessible* activeDescendant = GetAccessible(activeDescendantElm); michael@0: if (activeDescendant) { michael@0: FocusMgr()->ActiveItemChanged(activeDescendant, false); michael@0: #ifdef A11Y_LOG michael@0: if (logging::IsEnabled(logging::eFocus)) michael@0: logging::ActiveItemChangeCausedBy("ARIA activedescedant changed", michael@0: activeDescendant); michael@0: #endif michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: void michael@0: DocAccessible::ContentAppended(nsIDocument* aDocument, michael@0: nsIContent* aContainer, michael@0: nsIContent* aFirstNewContent, michael@0: int32_t /* unused */) michael@0: { michael@0: } michael@0: michael@0: void michael@0: DocAccessible::ContentStateChanged(nsIDocument* aDocument, michael@0: nsIContent* aContent, michael@0: EventStates aStateMask) michael@0: { michael@0: Accessible* accessible = GetAccessible(aContent); michael@0: if (!accessible) michael@0: return; michael@0: michael@0: if (aStateMask.HasState(NS_EVENT_STATE_CHECKED)) { michael@0: Accessible* widget = accessible->ContainerWidget(); michael@0: if (widget && widget->IsSelect()) { michael@0: AccSelChangeEvent::SelChangeType selChangeType = michael@0: aContent->AsElement()->State().HasState(NS_EVENT_STATE_CHECKED) ? michael@0: AccSelChangeEvent::eSelectionAdd : AccSelChangeEvent::eSelectionRemove; michael@0: nsRefPtr event = michael@0: new AccSelChangeEvent(widget, accessible, selChangeType); michael@0: FireDelayedEvent(event); michael@0: return; michael@0: } michael@0: michael@0: nsRefPtr event = michael@0: new AccStateChangeEvent(accessible, states::CHECKED, michael@0: aContent->AsElement()->State().HasState(NS_EVENT_STATE_CHECKED)); michael@0: FireDelayedEvent(event); michael@0: } michael@0: michael@0: if (aStateMask.HasState(NS_EVENT_STATE_INVALID)) { michael@0: nsRefPtr event = michael@0: new AccStateChangeEvent(accessible, states::INVALID, true); michael@0: FireDelayedEvent(event); michael@0: } michael@0: michael@0: if (aStateMask.HasState(NS_EVENT_STATE_VISITED)) { michael@0: nsRefPtr event = michael@0: new AccStateChangeEvent(accessible, states::TRAVERSED, true); michael@0: FireDelayedEvent(event); michael@0: } michael@0: } michael@0: michael@0: void michael@0: DocAccessible::DocumentStatesChanged(nsIDocument* aDocument, michael@0: EventStates aStateMask) michael@0: { michael@0: } michael@0: michael@0: void michael@0: DocAccessible::CharacterDataWillChange(nsIDocument* aDocument, michael@0: nsIContent* aContent, michael@0: CharacterDataChangeInfo* aInfo) michael@0: { michael@0: } michael@0: michael@0: void michael@0: DocAccessible::CharacterDataChanged(nsIDocument* aDocument, michael@0: nsIContent* aContent, michael@0: CharacterDataChangeInfo* aInfo) michael@0: { michael@0: } michael@0: michael@0: void michael@0: DocAccessible::ContentInserted(nsIDocument* aDocument, nsIContent* aContainer, michael@0: nsIContent* aChild, int32_t /* unused */) michael@0: { michael@0: } michael@0: michael@0: void michael@0: DocAccessible::ContentRemoved(nsIDocument* aDocument, nsIContent* aContainer, michael@0: nsIContent* aChild, int32_t /* unused */, michael@0: nsIContent* aPreviousSibling) michael@0: { michael@0: } michael@0: michael@0: void michael@0: DocAccessible::ParentChainChanged(nsIContent* aContent) michael@0: { michael@0: } michael@0: michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // Accessible michael@0: michael@0: #ifdef A11Y_LOG michael@0: nsresult michael@0: DocAccessible::HandleAccEvent(AccEvent* aEvent) michael@0: { michael@0: if (logging::IsEnabled(logging::eDocLoad)) michael@0: logging::DocLoadEventHandled(aEvent); michael@0: michael@0: return HyperTextAccessible::HandleAccEvent(aEvent); michael@0: } michael@0: #endif michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // Public members michael@0: michael@0: void* michael@0: DocAccessible::GetNativeWindow() const michael@0: { michael@0: if (!mPresShell) michael@0: return nullptr; michael@0: michael@0: nsViewManager* vm = mPresShell->GetViewManager(); michael@0: if (!vm) michael@0: return nullptr; michael@0: michael@0: nsCOMPtr widget; michael@0: vm->GetRootWidget(getter_AddRefs(widget)); michael@0: if (widget) michael@0: return widget->GetNativeData(NS_NATIVE_WINDOW); michael@0: michael@0: return nullptr; michael@0: } michael@0: michael@0: Accessible* michael@0: DocAccessible::GetAccessibleByUniqueIDInSubtree(void* aUniqueID) michael@0: { michael@0: Accessible* child = GetAccessibleByUniqueID(aUniqueID); michael@0: if (child) michael@0: return child; michael@0: michael@0: uint32_t childDocCount = mChildDocuments.Length(); michael@0: for (uint32_t childDocIdx= 0; childDocIdx < childDocCount; childDocIdx++) { michael@0: DocAccessible* childDocument = mChildDocuments.ElementAt(childDocIdx); michael@0: child = childDocument->GetAccessibleByUniqueIDInSubtree(aUniqueID); michael@0: if (child) michael@0: return child; michael@0: } michael@0: michael@0: return nullptr; michael@0: } michael@0: michael@0: Accessible* michael@0: DocAccessible::GetAccessibleOrContainer(nsINode* aNode) const michael@0: { michael@0: if (!aNode || !aNode->IsInDoc()) michael@0: return nullptr; michael@0: michael@0: nsINode* currNode = aNode; michael@0: Accessible* accessible = nullptr; michael@0: while (!(accessible = GetAccessible(currNode)) && michael@0: (currNode = currNode->GetParentNode())); michael@0: michael@0: return accessible; michael@0: } michael@0: michael@0: Accessible* michael@0: DocAccessible::GetAccessibleOrDescendant(nsINode* aNode) const michael@0: { michael@0: Accessible* acc = GetAccessible(aNode); michael@0: if (acc) michael@0: return acc; michael@0: michael@0: acc = GetContainerAccessible(aNode); michael@0: if (acc) { michael@0: uint32_t childCnt = acc->ChildCount(); michael@0: for (uint32_t idx = 0; idx < childCnt; idx++) { michael@0: Accessible* child = acc->GetChildAt(idx); michael@0: for (nsIContent* elm = child->GetContent(); michael@0: elm && elm != acc->GetContent(); michael@0: elm = elm->GetFlattenedTreeParent()) { michael@0: if (elm == aNode) michael@0: return child; michael@0: } michael@0: } michael@0: } michael@0: michael@0: return nullptr; michael@0: } michael@0: michael@0: void michael@0: DocAccessible::BindToDocument(Accessible* aAccessible, michael@0: nsRoleMapEntry* aRoleMapEntry) michael@0: { michael@0: // Put into DOM node cache. michael@0: if (aAccessible->IsNodeMapEntry()) michael@0: mNodeToAccessibleMap.Put(aAccessible->GetNode(), aAccessible); michael@0: michael@0: // Put into unique ID cache. michael@0: mAccessibleCache.Put(aAccessible->UniqueID(), aAccessible); michael@0: michael@0: aAccessible->SetRoleMapEntry(aRoleMapEntry); michael@0: michael@0: nsIContent* content = aAccessible->GetContent(); michael@0: if (content && content->IsElement()) michael@0: AddDependentIDsFor(content->AsElement()); michael@0: } michael@0: michael@0: void michael@0: DocAccessible::UnbindFromDocument(Accessible* aAccessible) michael@0: { michael@0: NS_ASSERTION(mAccessibleCache.GetWeak(aAccessible->UniqueID()), michael@0: "Unbinding the unbound accessible!"); michael@0: michael@0: // Fire focus event on accessible having DOM focus if active item was removed michael@0: // from the tree. michael@0: if (FocusMgr()->IsActiveItem(aAccessible)) { michael@0: FocusMgr()->ActiveItemChanged(nullptr); michael@0: #ifdef A11Y_LOG michael@0: if (logging::IsEnabled(logging::eFocus)) michael@0: logging::ActiveItemChangeCausedBy("tree shutdown", aAccessible); michael@0: #endif michael@0: } michael@0: michael@0: // Remove an accessible from node-to-accessible map if it exists there. michael@0: if (aAccessible->IsNodeMapEntry() && michael@0: mNodeToAccessibleMap.Get(aAccessible->GetNode()) == aAccessible) michael@0: mNodeToAccessibleMap.Remove(aAccessible->GetNode()); michael@0: michael@0: void* uniqueID = aAccessible->UniqueID(); michael@0: michael@0: NS_ASSERTION(!aAccessible->IsDefunct(), "Shutdown the shutdown accessible!"); michael@0: aAccessible->Shutdown(); michael@0: michael@0: mAccessibleCache.Remove(uniqueID); michael@0: } michael@0: michael@0: void michael@0: DocAccessible::ContentInserted(nsIContent* aContainerNode, michael@0: nsIContent* aStartChildNode, michael@0: nsIContent* aEndChildNode) michael@0: { michael@0: // Ignore content insertions until we constructed accessible tree. Otherwise michael@0: // schedule tree update on content insertion after layout. michael@0: if (mNotificationController && HasLoadState(eTreeConstructed)) { michael@0: // Update the whole tree of this document accessible when the container is michael@0: // null (document element is inserted or removed). michael@0: Accessible* container = aContainerNode ? michael@0: GetAccessibleOrContainer(aContainerNode) : this; michael@0: michael@0: mNotificationController->ScheduleContentInsertion(container, michael@0: aStartChildNode, michael@0: aEndChildNode); michael@0: } michael@0: } michael@0: michael@0: void michael@0: DocAccessible::ContentRemoved(nsIContent* aContainerNode, michael@0: nsIContent* aChildNode) michael@0: { michael@0: // Update the whole tree of this document accessible when the container is michael@0: // null (document element is removed). michael@0: Accessible* container = aContainerNode ? michael@0: GetAccessibleOrContainer(aContainerNode) : this; michael@0: michael@0: UpdateTree(container, aChildNode, false); michael@0: } michael@0: michael@0: void michael@0: DocAccessible::RecreateAccessible(nsIContent* aContent) michael@0: { michael@0: #ifdef A11Y_LOG michael@0: if (logging::IsEnabled(logging::eTree)) { michael@0: logging::MsgBegin("TREE", "accessible recreated"); michael@0: logging::Node("content", aContent); michael@0: logging::MsgEnd(); michael@0: } michael@0: #endif michael@0: michael@0: // XXX: we shouldn't recreate whole accessible subtree, instead we should michael@0: // subclass hide and show events to handle them separately and implement their michael@0: // coalescence with normal hide and show events. Note, in this case they michael@0: // should be coalesced with normal show/hide events. michael@0: michael@0: nsIContent* parent = aContent->GetFlattenedTreeParent(); michael@0: ContentRemoved(parent, aContent); michael@0: ContentInserted(parent, aContent, aContent->GetNextSibling()); michael@0: } michael@0: michael@0: void michael@0: DocAccessible::ProcessInvalidationList() michael@0: { michael@0: // Invalidate children of container accessible for each element in michael@0: // invalidation list. Allow invalidation list insertions while container michael@0: // children are recached. michael@0: for (uint32_t idx = 0; idx < mInvalidationList.Length(); idx++) { michael@0: nsIContent* content = mInvalidationList[idx]; michael@0: Accessible* accessible = GetAccessible(content); michael@0: if (!accessible) { michael@0: Accessible* container = GetContainerAccessible(content); michael@0: if (container) { michael@0: container->UpdateChildren(); michael@0: accessible = GetAccessible(content); michael@0: } michael@0: } michael@0: michael@0: // Make sure the subtree is created. michael@0: if (accessible) michael@0: CacheChildrenInSubtree(accessible); michael@0: } michael@0: michael@0: mInvalidationList.Clear(); michael@0: } michael@0: michael@0: Accessible* michael@0: DocAccessible::GetAccessibleEvenIfNotInMap(nsINode* aNode) const michael@0: { michael@0: if (!aNode->IsContent() || !aNode->AsContent()->IsHTML(nsGkAtoms::area)) michael@0: return GetAccessible(aNode); michael@0: michael@0: // XXX Bug 135040, incorrect when multiple images use the same map. michael@0: nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame(); michael@0: nsImageFrame* imageFrame = do_QueryFrame(frame); michael@0: if (imageFrame) { michael@0: Accessible* parent = GetAccessible(imageFrame->GetContent()); michael@0: if (parent) { michael@0: Accessible* area = michael@0: parent->AsImageMap()->GetChildAccessibleFor(aNode); michael@0: if (area) michael@0: return area; michael@0: michael@0: return nullptr; michael@0: } michael@0: } michael@0: michael@0: return GetAccessible(aNode); michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // Accessible protected michael@0: michael@0: void michael@0: DocAccessible::CacheChildren() michael@0: { michael@0: // Search for accessible children starting from the document element since michael@0: // some web pages tend to insert elements under it rather than document body. michael@0: dom::Element* rootElm = mDocumentNode->GetRootElement(); michael@0: if (!rootElm) michael@0: return; michael@0: michael@0: // Ignore last HTML:br, copied from HyperTextAccessible. michael@0: TreeWalker walker(this, rootElm); michael@0: Accessible* lastChild = nullptr; michael@0: while (Accessible* child = walker.NextChild()) { michael@0: if (lastChild) michael@0: AppendChild(lastChild); michael@0: michael@0: lastChild = child; michael@0: } michael@0: michael@0: if (lastChild) { michael@0: if (lastChild->IsHTMLBr()) michael@0: Document()->UnbindFromDocument(lastChild); michael@0: else michael@0: AppendChild(lastChild); michael@0: } michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // Protected members michael@0: michael@0: void michael@0: DocAccessible::NotifyOfLoading(bool aIsReloading) michael@0: { michael@0: // Mark the document accessible as loading, if it stays alive then we'll mark michael@0: // it as loaded when we receive proper notification. michael@0: mLoadState &= ~eDOMLoaded; michael@0: michael@0: if (!IsLoadEventTarget()) michael@0: return; michael@0: michael@0: if (aIsReloading) { michael@0: // Fire reload and state busy events on existing document accessible while michael@0: // event from user input flag can be calculated properly and accessible michael@0: // is alive. When new document gets loaded then this one is destroyed. michael@0: nsRefPtr reloadEvent = michael@0: new AccEvent(nsIAccessibleEvent::EVENT_DOCUMENT_RELOAD, this); michael@0: nsEventShell::FireEvent(reloadEvent); michael@0: } michael@0: michael@0: // Fire state busy change event. Use delayed event since we don't care michael@0: // actually if event isn't delivered when the document goes away like a shot. michael@0: nsRefPtr stateEvent = michael@0: new AccStateChangeEvent(this, states::BUSY, true); michael@0: FireDelayedEvent(stateEvent); michael@0: } michael@0: michael@0: void michael@0: DocAccessible::DoInitialUpdate() michael@0: { michael@0: if (nsCoreUtils::IsTabDocument(mDocumentNode)) michael@0: mDocFlags |= eTabDocument; michael@0: michael@0: mLoadState |= eTreeConstructed; michael@0: michael@0: // The content element may be changed before the initial update and then we michael@0: // miss the notification (since content tree change notifications are ignored michael@0: // prior to initial update). Make sure the content element is valid. michael@0: nsIContent* contentElm = nsCoreUtils::GetRoleContent(mDocumentNode); michael@0: if (mContent != contentElm) { michael@0: mContent = contentElm; michael@0: SetRoleMapEntry(aria::GetRoleMap(mContent)); michael@0: } michael@0: michael@0: // Build initial tree. michael@0: CacheChildrenInSubtree(this); michael@0: michael@0: // Fire reorder event after the document tree is constructed. Note, since michael@0: // this reorder event is processed by parent document then events targeted to michael@0: // this document may be fired prior to this reorder event. If this is michael@0: // a problem then consider to keep event processing per tab document. michael@0: if (!IsRoot()) { michael@0: nsRefPtr reorderEvent = new AccReorderEvent(Parent()); michael@0: ParentDocument()->FireDelayedEvent(reorderEvent); michael@0: } michael@0: } michael@0: michael@0: void michael@0: DocAccessible::ProcessLoad() michael@0: { michael@0: mLoadState |= eCompletelyLoaded; michael@0: michael@0: #ifdef A11Y_LOG michael@0: if (logging::IsEnabled(logging::eDocLoad)) michael@0: logging::DocCompleteLoad(this, IsLoadEventTarget()); michael@0: #endif michael@0: michael@0: // Do not fire document complete/stop events for root chrome document michael@0: // accessibles and for frame/iframe documents because michael@0: // a) screen readers start working on focus event in the case of root chrome michael@0: // documents michael@0: // b) document load event on sub documents causes screen readers to act is if michael@0: // entire page is reloaded. michael@0: if (!IsLoadEventTarget()) michael@0: return; michael@0: michael@0: // Fire complete/load stopped if the load event type is given. michael@0: if (mLoadEventType) { michael@0: nsRefPtr loadEvent = new AccEvent(mLoadEventType, this); michael@0: FireDelayedEvent(loadEvent); michael@0: michael@0: mLoadEventType = 0; michael@0: } michael@0: michael@0: // Fire busy state change event. michael@0: nsRefPtr stateEvent = michael@0: new AccStateChangeEvent(this, states::BUSY, false); michael@0: FireDelayedEvent(stateEvent); michael@0: } michael@0: michael@0: void michael@0: DocAccessible::AddDependentIDsFor(dom::Element* aRelProviderElm, michael@0: nsIAtom* aRelAttr) michael@0: { michael@0: for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) { michael@0: nsIAtom* relAttr = *kRelationAttrs[idx]; michael@0: if (aRelAttr && aRelAttr != relAttr) michael@0: continue; michael@0: michael@0: if (relAttr == nsGkAtoms::_for) { michael@0: if (!aRelProviderElm->IsHTML() || michael@0: (aRelProviderElm->Tag() != nsGkAtoms::label && michael@0: aRelProviderElm->Tag() != nsGkAtoms::output)) michael@0: continue; michael@0: michael@0: } else if (relAttr == nsGkAtoms::control) { michael@0: if (!aRelProviderElm->IsXUL() || michael@0: (aRelProviderElm->Tag() != nsGkAtoms::label && michael@0: aRelProviderElm->Tag() != nsGkAtoms::description)) michael@0: continue; michael@0: } michael@0: michael@0: IDRefsIterator iter(this, aRelProviderElm, relAttr); michael@0: while (true) { michael@0: const nsDependentSubstring id = iter.NextID(); michael@0: if (id.IsEmpty()) michael@0: break; michael@0: michael@0: AttrRelProviderArray* providers = mDependentIDsHash.Get(id); michael@0: if (!providers) { michael@0: providers = new AttrRelProviderArray(); michael@0: if (providers) { michael@0: mDependentIDsHash.Put(id, providers); michael@0: } michael@0: } michael@0: michael@0: if (providers) { michael@0: AttrRelProvider* provider = michael@0: new AttrRelProvider(relAttr, aRelProviderElm); michael@0: if (provider) { michael@0: providers->AppendElement(provider); michael@0: michael@0: // We've got here during the children caching. If the referenced michael@0: // content is not accessible then store it to pend its container michael@0: // children invalidation (this happens immediately after the caching michael@0: // is finished). michael@0: nsIContent* dependentContent = iter.GetElem(id); michael@0: if (dependentContent && !HasAccessible(dependentContent)) { michael@0: mInvalidationList.AppendElement(dependentContent); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: // If the relation attribute is given then we don't have anything else to michael@0: // check. michael@0: if (aRelAttr) michael@0: break; michael@0: } michael@0: } michael@0: michael@0: void michael@0: DocAccessible::RemoveDependentIDsFor(dom::Element* aRelProviderElm, michael@0: nsIAtom* aRelAttr) michael@0: { michael@0: for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) { michael@0: nsIAtom* relAttr = *kRelationAttrs[idx]; michael@0: if (aRelAttr && aRelAttr != *kRelationAttrs[idx]) michael@0: continue; michael@0: michael@0: IDRefsIterator iter(this, aRelProviderElm, relAttr); michael@0: while (true) { michael@0: const nsDependentSubstring id = iter.NextID(); michael@0: if (id.IsEmpty()) michael@0: break; michael@0: michael@0: AttrRelProviderArray* providers = mDependentIDsHash.Get(id); michael@0: if (providers) { michael@0: for (uint32_t jdx = 0; jdx < providers->Length(); ) { michael@0: AttrRelProvider* provider = (*providers)[jdx]; michael@0: if (provider->mRelAttr == relAttr && michael@0: provider->mContent == aRelProviderElm) michael@0: providers->RemoveElement(provider); michael@0: else michael@0: jdx++; michael@0: } michael@0: if (providers->Length() == 0) michael@0: mDependentIDsHash.Remove(id); michael@0: } michael@0: } michael@0: michael@0: // If the relation attribute is given then we don't have anything else to michael@0: // check. michael@0: if (aRelAttr) michael@0: break; michael@0: } michael@0: } michael@0: michael@0: bool michael@0: DocAccessible::UpdateAccessibleOnAttrChange(dom::Element* aElement, michael@0: nsIAtom* aAttribute) michael@0: { michael@0: if (aAttribute == nsGkAtoms::role) { michael@0: // It is common for js libraries to set the role on the body element after michael@0: // the document has loaded. In this case we just update the role map entry. michael@0: if (mContent == aElement) { michael@0: SetRoleMapEntry(aria::GetRoleMap(aElement)); michael@0: return true; michael@0: } michael@0: michael@0: // Recreate the accessible when role is changed because we might require a michael@0: // different accessible class for the new role or the accessible may expose michael@0: // a different sets of interfaces (COM restriction). michael@0: RecreateAccessible(aElement); michael@0: michael@0: return true; michael@0: } michael@0: michael@0: if (aAttribute == nsGkAtoms::href || michael@0: aAttribute == nsGkAtoms::onclick) { michael@0: // Not worth the expense to ensure which namespace these are in. It doesn't michael@0: // kill use to recreate the accessible even if the attribute was used in michael@0: // the wrong namespace or an element that doesn't support it. michael@0: michael@0: // Make sure the accessible is recreated asynchronously to allow the content michael@0: // to handle the attribute change. michael@0: RecreateAccessible(aElement); michael@0: return true; michael@0: } michael@0: michael@0: if (aAttribute == nsGkAtoms::aria_multiselectable && michael@0: aElement->HasAttr(kNameSpaceID_None, nsGkAtoms::role)) { michael@0: // This affects whether the accessible supports SelectAccessible. michael@0: // COM says we cannot change what interfaces are supported on-the-fly, michael@0: // so invalidate this object. A new one will be created on demand. michael@0: RecreateAccessible(aElement); michael@0: michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: void michael@0: DocAccessible::ProcessContentInserted(Accessible* aContainer, michael@0: const nsTArray >* aInsertedContent) michael@0: { michael@0: // Process insertions if the container accessible is still in tree. michael@0: if (!HasAccessible(aContainer->GetNode())) michael@0: return; michael@0: michael@0: bool containerNotUpdated = true; michael@0: michael@0: for (uint32_t idx = 0; idx < aInsertedContent->Length(); idx++) { michael@0: // The container might be changed, for example, because of the subsequent michael@0: // overlapping content insertion (i.e. other content was inserted between michael@0: // this inserted content and its container or the content was reinserted michael@0: // into different container of unrelated part of tree). To avoid a double michael@0: // processing of the content insertion ignore this insertion notification. michael@0: // Note, the inserted content might be not in tree at all at this point what michael@0: // means there's no container. Ignore the insertion too. michael@0: michael@0: Accessible* presentContainer = michael@0: GetContainerAccessible(aInsertedContent->ElementAt(idx)); michael@0: if (presentContainer != aContainer) michael@0: continue; michael@0: michael@0: if (containerNotUpdated) { michael@0: containerNotUpdated = false; michael@0: michael@0: if (aContainer == this) { michael@0: // If new root content has been inserted then update it. michael@0: nsIContent* rootContent = nsCoreUtils::GetRoleContent(mDocumentNode); michael@0: if (rootContent != mContent) { michael@0: mContent = rootContent; michael@0: SetRoleMapEntry(aria::GetRoleMap(mContent)); michael@0: } michael@0: michael@0: // Continue to update the tree even if we don't have root content. michael@0: // For example, elements may be inserted under the document element while michael@0: // there is no HTML body element. michael@0: } michael@0: michael@0: // XXX: Invalidate parent-child relations for container accessible and its michael@0: // children because there's no good way to find insertion point of new child michael@0: // accessibles into accessible tree. We need to invalidate children even michael@0: // there's no inserted accessibles in the end because accessible children michael@0: // are created while parent recaches child accessibles. michael@0: aContainer->InvalidateChildren(); michael@0: CacheChildrenInSubtree(aContainer); michael@0: } michael@0: michael@0: UpdateTree(aContainer, aInsertedContent->ElementAt(idx), true); michael@0: } michael@0: } michael@0: michael@0: void michael@0: DocAccessible::UpdateTree(Accessible* aContainer, nsIContent* aChildNode, michael@0: bool aIsInsert) michael@0: { michael@0: uint32_t updateFlags = eNoAccessible; michael@0: michael@0: // If child node is not accessible then look for its accessible children. michael@0: Accessible* child = GetAccessible(aChildNode); michael@0: #ifdef A11Y_LOG michael@0: if (logging::IsEnabled(logging::eTree)) { michael@0: logging::MsgBegin("TREE", "process content %s", michael@0: (aIsInsert ? "insertion" : "removal")); michael@0: logging::Node("container", aContainer->GetNode()); michael@0: logging::Node("child", aChildNode); michael@0: if (child) michael@0: logging::Address("child", child); michael@0: else michael@0: logging::MsgEntry("child accessible: null"); michael@0: michael@0: logging::MsgEnd(); michael@0: } michael@0: #endif michael@0: michael@0: nsRefPtr reorderEvent = new AccReorderEvent(aContainer); michael@0: michael@0: if (child) { michael@0: updateFlags |= UpdateTreeInternal(child, aIsInsert, reorderEvent); michael@0: } else { michael@0: if (aIsInsert) { michael@0: TreeWalker walker(aContainer, aChildNode, TreeWalker::eWalkCache); michael@0: michael@0: while ((child = walker.NextChild())) michael@0: updateFlags |= UpdateTreeInternal(child, aIsInsert, reorderEvent); michael@0: } else { michael@0: // aChildNode may not coorespond to a particular accessible, to handle michael@0: // this we go through all the children of aContainer. Then if a child michael@0: // has aChildNode as an ancestor, or does not have the node for michael@0: // aContainer as an ancestor remove that child of aContainer. Note that michael@0: // when we are called aChildNode may already have been removed michael@0: // from the DOM so we can't expect it to have a parent or what was it's michael@0: // parent to have it as a child. michael@0: nsINode* containerNode = aContainer->GetNode(); michael@0: for (uint32_t idx = 0; idx < aContainer->ContentChildCount();) { michael@0: Accessible* child = aContainer->ContentChildAt(idx); michael@0: michael@0: // If accessible doesn't have its own content then we assume parent michael@0: // will handle its update. If child is DocAccessible then we don't michael@0: // handle updating it here either. michael@0: if (!child->HasOwnContent() || child->IsDoc()) { michael@0: idx++; michael@0: continue; michael@0: } michael@0: michael@0: nsINode* childNode = child->GetContent(); michael@0: while (childNode != aChildNode && childNode != containerNode && michael@0: (childNode = childNode->GetParentNode())); michael@0: michael@0: if (childNode != containerNode) { michael@0: updateFlags |= UpdateTreeInternal(child, false, reorderEvent); michael@0: } else { michael@0: idx++; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Content insertion/removal is not cause of accessible tree change. michael@0: if (updateFlags == eNoAccessible) michael@0: return; michael@0: michael@0: // Check to see if change occurred inside an alert, and fire an EVENT_ALERT michael@0: // if it did. michael@0: if (aIsInsert && !(updateFlags & eAlertAccessible)) { michael@0: // XXX: tree traversal is perf issue, accessible should know if they are michael@0: // children of alert accessible to avoid this. michael@0: Accessible* ancestor = aContainer; michael@0: while (ancestor) { michael@0: if (ancestor->ARIARole() == roles::ALERT) { michael@0: FireDelayedEvent(nsIAccessibleEvent::EVENT_ALERT, ancestor); michael@0: break; michael@0: } michael@0: michael@0: // Don't climb above this document. michael@0: if (ancestor == this) michael@0: break; michael@0: michael@0: ancestor = ancestor->Parent(); michael@0: } michael@0: } michael@0: michael@0: MaybeNotifyOfValueChange(aContainer); michael@0: michael@0: // Fire reorder event so the MSAA clients know the children have changed. Also michael@0: // the event is used internally by MSAA layer. michael@0: FireDelayedEvent(reorderEvent); michael@0: } michael@0: michael@0: uint32_t michael@0: DocAccessible::UpdateTreeInternal(Accessible* aChild, bool aIsInsert, michael@0: AccReorderEvent* aReorderEvent) michael@0: { michael@0: uint32_t updateFlags = eAccessible; michael@0: michael@0: // If a focused node has been shown then it could mean its frame was recreated michael@0: // while the node stays focused and we need to fire focus event on michael@0: // the accessible we just created. If the queue contains a focus event for michael@0: // this node already then it will be suppressed by this one. michael@0: Accessible* focusedAcc = nullptr; michael@0: michael@0: nsINode* node = aChild->GetNode(); michael@0: if (aIsInsert) { michael@0: // Create accessible tree for shown accessible. michael@0: CacheChildrenInSubtree(aChild, &focusedAcc); michael@0: michael@0: } else { michael@0: // Fire menupopup end event before hide event if a menu goes away. michael@0: michael@0: // XXX: We don't look into children of hidden subtree to find hiding michael@0: // menupopup (as we did prior bug 570275) because we don't do that when michael@0: // menu is showing (and that's impossible until bug 606924 is fixed). michael@0: // Nevertheless we should do this at least because layout coalesces michael@0: // the changes before our processing and we may miss some menupopup michael@0: // events. Now we just want to be consistent in content insertion/removal michael@0: // handling. michael@0: if (aChild->ARIARole() == roles::MENUPOPUP) michael@0: FireDelayedEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_END, aChild); michael@0: } michael@0: michael@0: // Fire show/hide event. michael@0: nsRefPtr event; michael@0: if (aIsInsert) michael@0: event = new AccShowEvent(aChild, node); michael@0: else michael@0: event = new AccHideEvent(aChild, node); michael@0: michael@0: FireDelayedEvent(event); michael@0: aReorderEvent->AddSubMutationEvent(event); michael@0: michael@0: if (aIsInsert) { michael@0: roles::Role ariaRole = aChild->ARIARole(); michael@0: if (ariaRole == roles::MENUPOPUP) { michael@0: // Fire EVENT_MENUPOPUP_START if ARIA menu appears. michael@0: FireDelayedEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_START, aChild); michael@0: michael@0: } else if (ariaRole == roles::ALERT) { michael@0: // Fire EVENT_ALERT if ARIA alert appears. michael@0: updateFlags = eAlertAccessible; michael@0: FireDelayedEvent(nsIAccessibleEvent::EVENT_ALERT, aChild); michael@0: } michael@0: } else { michael@0: // Update the tree for content removal. michael@0: // The accessible parent may differ from container accessible if michael@0: // the parent doesn't have own DOM node like list accessible for HTML michael@0: // selects. michael@0: Accessible* parent = aChild->Parent(); michael@0: NS_ASSERTION(parent, "No accessible parent?!"); michael@0: if (parent) michael@0: parent->RemoveChild(aChild); michael@0: michael@0: UncacheChildrenInSubtree(aChild); michael@0: } michael@0: michael@0: // XXX: do we really want to send focus to focused DOM node not taking into michael@0: // account active item? michael@0: if (focusedAcc) { michael@0: FocusMgr()->DispatchFocusEvent(this, focusedAcc); michael@0: SelectionMgr()->SetControlSelectionListener(focusedAcc->GetNode()->AsElement()); michael@0: } michael@0: michael@0: return updateFlags; michael@0: } michael@0: michael@0: void michael@0: DocAccessible::CacheChildrenInSubtree(Accessible* aRoot, michael@0: Accessible** aFocusedAcc) michael@0: { michael@0: // If the accessible is focused then report a focus event after all related michael@0: // mutation events. michael@0: if (aFocusedAcc && !*aFocusedAcc && michael@0: FocusMgr()->HasDOMFocus(aRoot->GetContent())) michael@0: *aFocusedAcc = aRoot; michael@0: michael@0: aRoot->EnsureChildren(); michael@0: michael@0: // Make sure we create accessible tree defined in DOM only, i.e. if accessible michael@0: // provides specific tree (like XUL trees) then tree creation is handled by michael@0: // this accessible. michael@0: uint32_t count = aRoot->ContentChildCount(); michael@0: for (uint32_t idx = 0; idx < count; idx++) { michael@0: Accessible* child = aRoot->ContentChildAt(idx); michael@0: NS_ASSERTION(child, "Illicit tree change while tree is created!"); michael@0: // Don't cross document boundaries. michael@0: if (child && child->IsContent()) michael@0: CacheChildrenInSubtree(child, aFocusedAcc); michael@0: } michael@0: michael@0: // Fire document load complete on ARIA documents. michael@0: // XXX: we should delay an event if the ARIA document has aria-busy. michael@0: if (aRoot->HasARIARole() && !aRoot->IsDoc()) { michael@0: a11y::role role = aRoot->ARIARole(); michael@0: if (role == roles::DIALOG || role == roles::DOCUMENT) michael@0: FireDelayedEvent(nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE, aRoot); michael@0: } michael@0: } michael@0: michael@0: void michael@0: DocAccessible::UncacheChildrenInSubtree(Accessible* aRoot) michael@0: { michael@0: aRoot->mStateFlags |= eIsNotInDocument; michael@0: michael@0: nsIContent* rootContent = aRoot->GetContent(); michael@0: if (rootContent && rootContent->IsElement()) michael@0: RemoveDependentIDsFor(rootContent->AsElement()); michael@0: michael@0: uint32_t count = aRoot->ContentChildCount(); michael@0: for (uint32_t idx = 0; idx < count; idx++) michael@0: UncacheChildrenInSubtree(aRoot->ContentChildAt(idx)); michael@0: michael@0: if (aRoot->IsNodeMapEntry() && michael@0: mNodeToAccessibleMap.Get(aRoot->GetNode()) == aRoot) michael@0: mNodeToAccessibleMap.Remove(aRoot->GetNode()); michael@0: } michael@0: michael@0: void michael@0: DocAccessible::ShutdownChildrenInSubtree(Accessible* aAccessible) michael@0: { michael@0: // Traverse through children and shutdown them before this accessible. When michael@0: // child gets shutdown then it removes itself from children array of its michael@0: //parent. Use jdx index to process the cases if child is not attached to the michael@0: // parent and as result doesn't remove itself from its children. michael@0: uint32_t count = aAccessible->ContentChildCount(); michael@0: for (uint32_t idx = 0, jdx = 0; idx < count; idx++) { michael@0: Accessible* child = aAccessible->ContentChildAt(jdx); michael@0: if (!child->IsBoundToParent()) { michael@0: NS_ERROR("Parent refers to a child, child doesn't refer to parent!"); michael@0: jdx++; michael@0: } michael@0: michael@0: // Don't cross document boundaries. The outerdoc shutdown takes care about michael@0: // its subdocument. michael@0: if (!child->IsDoc()) michael@0: ShutdownChildrenInSubtree(child); michael@0: } michael@0: michael@0: UnbindFromDocument(aAccessible); michael@0: } michael@0: michael@0: bool michael@0: DocAccessible::IsLoadEventTarget() const michael@0: { michael@0: nsCOMPtr treeItem = mDocumentNode->GetDocShell(); michael@0: NS_ASSERTION(treeItem, "No document shell for document!"); michael@0: michael@0: nsCOMPtr parentTreeItem; michael@0: treeItem->GetParent(getter_AddRefs(parentTreeItem)); michael@0: michael@0: // Not a root document. michael@0: if (parentTreeItem) { michael@0: // Return true if it's either: michael@0: // a) tab document; michael@0: nsCOMPtr rootTreeItem; michael@0: treeItem->GetRootTreeItem(getter_AddRefs(rootTreeItem)); michael@0: if (parentTreeItem == rootTreeItem) michael@0: return true; michael@0: michael@0: // b) frame/iframe document and its parent document is not in loading state michael@0: // Note: we can get notifications while document is loading (and thus michael@0: // while there's no parent document yet). michael@0: DocAccessible* parentDoc = ParentDocument(); michael@0: return parentDoc && parentDoc->HasLoadState(eCompletelyLoaded); michael@0: } michael@0: michael@0: // It's content (not chrome) root document. michael@0: return (treeItem->ItemType() == nsIDocShellTreeItem::typeContent); michael@0: } michael@0: michael@0: PLDHashOperator michael@0: DocAccessible::CycleCollectorTraverseDepIDsEntry(const nsAString& aKey, michael@0: AttrRelProviderArray* aProviders, michael@0: void* aUserArg) michael@0: { michael@0: nsCycleCollectionTraversalCallback* cb = michael@0: static_cast(aUserArg); michael@0: michael@0: for (int32_t jdx = aProviders->Length() - 1; jdx >= 0; jdx--) { michael@0: NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(*cb, michael@0: "content of dependent ids hash entry of document accessible"); michael@0: michael@0: AttrRelProvider* provider = (*aProviders)[jdx]; michael@0: cb->NoteXPCOMChild(provider->mContent); michael@0: michael@0: NS_ASSERTION(provider->mContent->IsInDoc(), michael@0: "Referred content is not in document!"); michael@0: } michael@0: michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: