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 "NotificationController.h" michael@0: michael@0: #include "DocAccessible-inl.h" michael@0: #include "TextLeafAccessible.h" michael@0: #include "TextUpdater.h" michael@0: michael@0: #include "mozilla/dom/Element.h" michael@0: #include "mozilla/Telemetry.h" michael@0: michael@0: using namespace mozilla; michael@0: using namespace mozilla::a11y; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // NotificationCollector michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: michael@0: NotificationController::NotificationController(DocAccessible* aDocument, michael@0: nsIPresShell* aPresShell) : michael@0: EventQueue(aDocument), mObservingState(eNotObservingRefresh), michael@0: mPresShell(aPresShell) michael@0: { michael@0: // Schedule initial accessible tree construction. michael@0: ScheduleProcessing(); michael@0: } michael@0: michael@0: NotificationController::~NotificationController() michael@0: { michael@0: NS_ASSERTION(!mDocument, "Controller wasn't shutdown properly!"); michael@0: if (mDocument) michael@0: Shutdown(); michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // NotificationCollector: AddRef/Release and cycle collection michael@0: michael@0: NS_IMPL_CYCLE_COLLECTING_NATIVE_ADDREF(NotificationController) michael@0: NS_IMPL_CYCLE_COLLECTING_NATIVE_RELEASE(NotificationController) michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_CLASS(NotificationController) michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(NotificationController) michael@0: if (tmp->mDocument) michael@0: tmp->Shutdown(); michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK_END michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(NotificationController) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHangingChildDocuments) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContentInsertions) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEvents) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(NotificationController, AddRef) michael@0: NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(NotificationController, Release) michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // NotificationCollector: public michael@0: michael@0: void michael@0: NotificationController::Shutdown() michael@0: { michael@0: if (mObservingState != eNotObservingRefresh && michael@0: mPresShell->RemoveRefreshObserver(this, Flush_Display)) { michael@0: mObservingState = eNotObservingRefresh; michael@0: } michael@0: michael@0: // Shutdown handling child documents. michael@0: int32_t childDocCount = mHangingChildDocuments.Length(); michael@0: for (int32_t idx = childDocCount - 1; idx >= 0; idx--) { michael@0: if (!mHangingChildDocuments[idx]->IsDefunct()) michael@0: mHangingChildDocuments[idx]->Shutdown(); michael@0: } michael@0: michael@0: mHangingChildDocuments.Clear(); michael@0: michael@0: mDocument = nullptr; michael@0: mPresShell = nullptr; michael@0: michael@0: mTextHash.Clear(); michael@0: mContentInsertions.Clear(); michael@0: mNotifications.Clear(); michael@0: mEvents.Clear(); michael@0: } michael@0: michael@0: void michael@0: NotificationController::ScheduleChildDocBinding(DocAccessible* aDocument) michael@0: { michael@0: // Schedule child document binding to the tree. michael@0: mHangingChildDocuments.AppendElement(aDocument); michael@0: ScheduleProcessing(); michael@0: } michael@0: michael@0: void michael@0: NotificationController::ScheduleContentInsertion(Accessible* aContainer, michael@0: nsIContent* aStartChildNode, michael@0: nsIContent* aEndChildNode) michael@0: { michael@0: nsRefPtr insertion = new ContentInsertion(mDocument, michael@0: aContainer); michael@0: if (insertion && insertion->InitChildList(aStartChildNode, aEndChildNode) && michael@0: mContentInsertions.AppendElement(insertion)) { michael@0: ScheduleProcessing(); michael@0: } michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // NotificationCollector: protected michael@0: michael@0: void michael@0: NotificationController::ScheduleProcessing() michael@0: { michael@0: // If notification flush isn't planed yet start notification flush michael@0: // asynchronously (after style and layout). michael@0: if (mObservingState == eNotObservingRefresh) { michael@0: if (mPresShell->AddRefreshObserver(this, Flush_Display)) michael@0: mObservingState = eRefreshObserving; michael@0: } michael@0: } michael@0: michael@0: bool michael@0: NotificationController::IsUpdatePending() michael@0: { michael@0: return mPresShell->IsLayoutFlushObserver() || michael@0: mObservingState == eRefreshProcessingForUpdate || michael@0: mContentInsertions.Length() != 0 || mNotifications.Length() != 0 || michael@0: mTextHash.Count() != 0 || michael@0: !mDocument->HasLoadState(DocAccessible::eTreeConstructed); michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // NotificationCollector: private michael@0: michael@0: void michael@0: NotificationController::WillRefresh(mozilla::TimeStamp aTime) michael@0: { michael@0: Telemetry::AutoTimer updateTimer; michael@0: michael@0: // If the document accessible that notification collector was created for is michael@0: // now shut down, don't process notifications anymore. michael@0: NS_ASSERTION(mDocument, michael@0: "The document was shut down while refresh observer is attached!"); michael@0: if (!mDocument) michael@0: return; michael@0: michael@0: if (mObservingState == eRefreshProcessing || michael@0: mObservingState == eRefreshProcessingForUpdate) michael@0: return; michael@0: michael@0: // Any generic notifications should be queued if we're processing content michael@0: // insertions or generic notifications. michael@0: mObservingState = eRefreshProcessingForUpdate; michael@0: michael@0: // Initial accessible tree construction. michael@0: if (!mDocument->HasLoadState(DocAccessible::eTreeConstructed)) { michael@0: // If document is not bound to parent at this point then the document is not michael@0: // ready yet (process notifications later). michael@0: if (!mDocument->IsBoundToParent()) { michael@0: mObservingState = eRefreshObserving; michael@0: return; michael@0: } michael@0: michael@0: #ifdef A11Y_LOG michael@0: if (logging::IsEnabled(logging::eTree)) { michael@0: logging::MsgBegin("TREE", "initial tree created"); michael@0: logging::Address("document", mDocument); michael@0: logging::MsgEnd(); michael@0: } michael@0: #endif michael@0: michael@0: mDocument->DoInitialUpdate(); michael@0: michael@0: NS_ASSERTION(mContentInsertions.Length() == 0, michael@0: "Pending content insertions while initial accessible tree isn't created!"); michael@0: } michael@0: michael@0: // Initialize scroll support if needed. michael@0: if (!(mDocument->mDocFlags & DocAccessible::eScrollInitialized)) michael@0: mDocument->AddScrollListener(); michael@0: michael@0: // Process content inserted notifications to update the tree. Process other michael@0: // notifications like DOM events and then flush event queue. If any new michael@0: // notifications are queued during this processing then they will be processed michael@0: // on next refresh. If notification processing queues up new events then they michael@0: // are processed in this refresh. If events processing queues up new events michael@0: // then new events are processed on next refresh. michael@0: // Note: notification processing or event handling may shut down the owning michael@0: // document accessible. michael@0: michael@0: // Process only currently queued content inserted notifications. michael@0: nsTArray > contentInsertions; michael@0: contentInsertions.SwapElements(mContentInsertions); michael@0: michael@0: uint32_t insertionCount = contentInsertions.Length(); michael@0: for (uint32_t idx = 0; idx < insertionCount; idx++) { michael@0: contentInsertions[idx]->Process(); michael@0: if (!mDocument) michael@0: return; michael@0: } michael@0: michael@0: // Process rendered text change notifications. michael@0: mTextHash.EnumerateEntries(TextEnumerator, mDocument); michael@0: mTextHash.Clear(); michael@0: michael@0: // Bind hanging child documents. michael@0: uint32_t hangingDocCnt = mHangingChildDocuments.Length(); michael@0: for (uint32_t idx = 0; idx < hangingDocCnt; idx++) { michael@0: DocAccessible* childDoc = mHangingChildDocuments[idx]; michael@0: if (childDoc->IsDefunct()) michael@0: continue; michael@0: michael@0: nsIContent* ownerContent = mDocument->DocumentNode()-> michael@0: FindContentForSubDocument(childDoc->DocumentNode()); michael@0: if (ownerContent) { michael@0: Accessible* outerDocAcc = mDocument->GetAccessible(ownerContent); michael@0: if (outerDocAcc && outerDocAcc->AppendChild(childDoc)) { michael@0: if (mDocument->AppendChildDocument(childDoc)) michael@0: continue; michael@0: michael@0: outerDocAcc->RemoveChild(childDoc); michael@0: } michael@0: michael@0: // Failed to bind the child document, destroy it. michael@0: childDoc->Shutdown(); michael@0: } michael@0: } michael@0: mHangingChildDocuments.Clear(); michael@0: michael@0: // If the document is ready and all its subdocuments are completely loaded michael@0: // then process the document load. michael@0: if (mDocument->HasLoadState(DocAccessible::eReady) && michael@0: !mDocument->HasLoadState(DocAccessible::eCompletelyLoaded) && michael@0: hangingDocCnt == 0) { michael@0: uint32_t childDocCnt = mDocument->ChildDocumentCount(), childDocIdx = 0; michael@0: for (; childDocIdx < childDocCnt; childDocIdx++) { michael@0: DocAccessible* childDoc = mDocument->GetChildDocumentAt(childDocIdx); michael@0: if (!childDoc->HasLoadState(DocAccessible::eCompletelyLoaded)) michael@0: break; michael@0: } michael@0: michael@0: if (childDocIdx == childDocCnt) { michael@0: mDocument->ProcessLoad(); michael@0: if (!mDocument) michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // Process only currently queued generic notifications. michael@0: nsTArray < nsRefPtr > notifications; michael@0: notifications.SwapElements(mNotifications); michael@0: michael@0: uint32_t notificationCount = notifications.Length(); michael@0: for (uint32_t idx = 0; idx < notificationCount; idx++) { michael@0: notifications[idx]->Process(); michael@0: if (!mDocument) michael@0: return; michael@0: } michael@0: michael@0: // Process invalidation list of the document after all accessible tree michael@0: // modification are done. michael@0: mDocument->ProcessInvalidationList(); michael@0: michael@0: // If a generic notification occurs after this point then we may be allowed to michael@0: // process it synchronously. However we do not want to reenter if fireing michael@0: // events causes script to run. michael@0: mObservingState = eRefreshProcessing; michael@0: michael@0: ProcessEventQueue(); michael@0: mObservingState = eRefreshObserving; michael@0: if (!mDocument) michael@0: return; michael@0: michael@0: // Stop further processing if there are no new notifications of any kind or michael@0: // events and document load is processed. michael@0: if (mContentInsertions.IsEmpty() && mNotifications.IsEmpty() && michael@0: mEvents.IsEmpty() && mTextHash.Count() == 0 && michael@0: mHangingChildDocuments.IsEmpty() && michael@0: mDocument->HasLoadState(DocAccessible::eCompletelyLoaded) && michael@0: mPresShell->RemoveRefreshObserver(this, Flush_Display)) { michael@0: mObservingState = eNotObservingRefresh; michael@0: } michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // Notification controller: text leaf accessible text update michael@0: michael@0: PLDHashOperator michael@0: NotificationController::TextEnumerator(nsCOMPtrHashKey* aEntry, michael@0: void* aUserArg) michael@0: { michael@0: DocAccessible* document = static_cast(aUserArg); michael@0: nsIContent* textNode = aEntry->GetKey(); michael@0: Accessible* textAcc = document->GetAccessible(textNode); michael@0: michael@0: // If the text node is not in tree or doesn't have frame then this case should michael@0: // have been handled already by content removal notifications. michael@0: nsINode* containerNode = textNode->GetParentNode(); michael@0: if (!containerNode) { michael@0: NS_ASSERTION(!textAcc, michael@0: "Text node was removed but accessible is kept alive!"); michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: nsIFrame* textFrame = textNode->GetPrimaryFrame(); michael@0: if (!textFrame) { michael@0: NS_ASSERTION(!textAcc, michael@0: "Text node isn't rendered but accessible is kept alive!"); michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: nsIContent* containerElm = containerNode->IsElement() ? michael@0: containerNode->AsElement() : nullptr; michael@0: michael@0: nsAutoString text; michael@0: textFrame->GetRenderedText(&text); michael@0: michael@0: // Remove text accessible if rendered text is empty. michael@0: if (textAcc) { michael@0: if (text.IsEmpty()) { michael@0: #ifdef A11Y_LOG michael@0: if (logging::IsEnabled(logging::eTree | logging::eText)) { michael@0: logging::MsgBegin("TREE", "text node lost its content"); michael@0: logging::Node("container", containerElm); michael@0: logging::Node("content", textNode); michael@0: logging::MsgEnd(); michael@0: } michael@0: #endif michael@0: michael@0: document->ContentRemoved(containerElm, textNode); michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: // Update text of the accessible and fire text change events. michael@0: #ifdef A11Y_LOG michael@0: if (logging::IsEnabled(logging::eText)) { michael@0: logging::MsgBegin("TEXT", "text may be changed"); michael@0: logging::Node("container", containerElm); michael@0: logging::Node("content", textNode); michael@0: logging::MsgEntry("old text '%s'", michael@0: NS_ConvertUTF16toUTF8(textAcc->AsTextLeaf()->Text()).get()); michael@0: logging::MsgEntry("new text: '%s'", michael@0: NS_ConvertUTF16toUTF8(text).get()); michael@0: logging::MsgEnd(); michael@0: } michael@0: #endif michael@0: michael@0: TextUpdater::Run(document, textAcc->AsTextLeaf(), text); michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: // Append an accessible if rendered text is not empty. michael@0: if (!text.IsEmpty()) { michael@0: #ifdef A11Y_LOG michael@0: if (logging::IsEnabled(logging::eTree | logging::eText)) { michael@0: logging::MsgBegin("TREE", "text node gains new content"); michael@0: logging::Node("container", containerElm); michael@0: logging::Node("content", textNode); michael@0: logging::MsgEnd(); michael@0: } michael@0: #endif michael@0: michael@0: // Make sure the text node is in accessible document still. michael@0: Accessible* container = document->GetAccessibleOrContainer(containerNode); michael@0: NS_ASSERTION(container, michael@0: "Text node having rendered text hasn't accessible document!"); michael@0: if (container) { michael@0: nsTArray > insertedContents; michael@0: insertedContents.AppendElement(textNode); michael@0: document->ProcessContentInserted(container, &insertedContents); michael@0: } michael@0: } michael@0: michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // NotificationController: content inserted notification michael@0: michael@0: NotificationController::ContentInsertion:: michael@0: ContentInsertion(DocAccessible* aDocument, Accessible* aContainer) : michael@0: mDocument(aDocument), mContainer(aContainer) michael@0: { michael@0: } michael@0: michael@0: bool michael@0: NotificationController::ContentInsertion:: michael@0: InitChildList(nsIContent* aStartChildNode, nsIContent* aEndChildNode) michael@0: { michael@0: bool haveToUpdate = false; michael@0: michael@0: nsIContent* node = aStartChildNode; michael@0: while (node != aEndChildNode) { michael@0: // Notification triggers for content insertion even if no content was michael@0: // actually inserted, check if the given content has a frame to discard michael@0: // this case early. michael@0: if (node->GetPrimaryFrame()) { michael@0: if (mInsertedContent.AppendElement(node)) michael@0: haveToUpdate = true; michael@0: } michael@0: michael@0: node = node->GetNextSibling(); michael@0: } michael@0: michael@0: return haveToUpdate; michael@0: } michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION(NotificationController::ContentInsertion, michael@0: mContainer) michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(NotificationController::ContentInsertion, michael@0: AddRef) michael@0: NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(NotificationController::ContentInsertion, michael@0: Release) michael@0: michael@0: void michael@0: NotificationController::ContentInsertion::Process() michael@0: { michael@0: mDocument->ProcessContentInserted(mContainer, &mInsertedContent); michael@0: michael@0: mDocument = nullptr; michael@0: mContainer = nullptr; michael@0: mInsertedContent.Clear(); michael@0: } michael@0: