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: