mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-26 06:11:37 +00:00
0293722fcc
This is an extension protocol that can be used for platform specific API (ie. AccessibleWrap methods). I'm not thrilled with the seperate constructor for the sub-protocol. This means that the parent won't have the actor upon DocAccessibleParent construction, and some timing bugs can arise because of the extra round trip. It would be cool if both actors could be co-created, but that would require ManagedEndpoint, and for PBrowser to manage them both. I don't want to expose this to PBrowser. Differential Revision: https://phabricator.services.mozilla.com/D37955 --HG-- extra : moz-landing-system : lando
970 lines
34 KiB
C++
970 lines
34 KiB
C++
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "NotificationController.h"
|
|
|
|
#include "DocAccessible-inl.h"
|
|
#include "DocAccessibleChild.h"
|
|
#include "nsEventShell.h"
|
|
#include "TextLeafAccessible.h"
|
|
#include "TextUpdater.h"
|
|
|
|
#include "mozilla/dom/BrowserChild.h"
|
|
#include "mozilla/dom/Element.h"
|
|
#include "mozilla/PresShell.h"
|
|
#include "mozilla/Telemetry.h"
|
|
|
|
using namespace mozilla;
|
|
using namespace mozilla::a11y;
|
|
using namespace mozilla::dom;
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// NotificationCollector
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
NotificationController::NotificationController(DocAccessible* aDocument,
|
|
PresShell* aPresShell)
|
|
: EventQueue(aDocument),
|
|
mObservingState(eNotObservingRefresh),
|
|
mPresShell(aPresShell),
|
|
mEventGeneration(0) {
|
|
#ifdef DEBUG
|
|
mMoveGuardOnStack = false;
|
|
#endif
|
|
|
|
// Schedule initial accessible tree construction.
|
|
ScheduleProcessing();
|
|
}
|
|
|
|
NotificationController::~NotificationController() {
|
|
NS_ASSERTION(!mDocument, "Controller wasn't shutdown properly!");
|
|
if (mDocument) Shutdown();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// NotificationCollector: AddRef/Release and cycle collection
|
|
|
|
NS_IMPL_CYCLE_COLLECTING_NATIVE_ADDREF(NotificationController)
|
|
NS_IMPL_CYCLE_COLLECTING_NATIVE_RELEASE(NotificationController)
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_CLASS(NotificationController)
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(NotificationController)
|
|
if (tmp->mDocument) tmp->Shutdown();
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(NotificationController)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHangingChildDocuments)
|
|
for (auto it = tmp->mContentInsertions.ConstIter(); !it.Done(); it.Next()) {
|
|
NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mContentInsertions key");
|
|
cb.NoteXPCOMChild(it.Key());
|
|
nsTArray<nsCOMPtr<nsIContent>>* list = it.UserData();
|
|
for (uint32_t i = 0; i < list->Length(); i++) {
|
|
NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mContentInsertions value item");
|
|
cb.NoteXPCOMChild(list->ElementAt(i));
|
|
}
|
|
}
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEvents)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRelocations)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(NotificationController, AddRef)
|
|
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(NotificationController, Release)
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// NotificationCollector: public
|
|
|
|
void NotificationController::Shutdown() {
|
|
if (mObservingState != eNotObservingRefresh &&
|
|
mPresShell->RemoveRefreshObserver(this, FlushType::Display)) {
|
|
mObservingState = eNotObservingRefresh;
|
|
}
|
|
|
|
// Shutdown handling child documents.
|
|
int32_t childDocCount = mHangingChildDocuments.Length();
|
|
for (int32_t idx = childDocCount - 1; idx >= 0; idx--) {
|
|
if (!mHangingChildDocuments[idx]->IsDefunct())
|
|
mHangingChildDocuments[idx]->Shutdown();
|
|
}
|
|
|
|
mHangingChildDocuments.Clear();
|
|
|
|
mDocument = nullptr;
|
|
mPresShell = nullptr;
|
|
|
|
mTextHash.Clear();
|
|
mContentInsertions.Clear();
|
|
mNotifications.Clear();
|
|
mEvents.Clear();
|
|
mRelocations.Clear();
|
|
mEventTree.Clear();
|
|
}
|
|
|
|
EventTree* NotificationController::QueueMutation(Accessible* aContainer) {
|
|
EventTree* tree = mEventTree.FindOrInsert(aContainer);
|
|
if (tree) {
|
|
ScheduleProcessing();
|
|
}
|
|
return tree;
|
|
}
|
|
|
|
bool NotificationController::QueueMutationEvent(AccTreeMutationEvent* aEvent) {
|
|
// We have to allow there to be a hide and then a show event for a target
|
|
// because of targets getting moved. However we need to coalesce a show and
|
|
// then a hide for a target which means we need to check for that here.
|
|
if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE &&
|
|
aEvent->GetAccessible()->ShowEventTarget()) {
|
|
AccTreeMutationEvent* showEvent =
|
|
mMutationMap.GetEvent(aEvent->GetAccessible(), EventMap::ShowEvent);
|
|
DropMutationEvent(showEvent);
|
|
return false;
|
|
}
|
|
|
|
AccMutationEvent* mutEvent = downcast_accEvent(aEvent);
|
|
mEventGeneration++;
|
|
mutEvent->SetEventGeneration(mEventGeneration);
|
|
|
|
if (!mFirstMutationEvent) {
|
|
mFirstMutationEvent = aEvent;
|
|
ScheduleProcessing();
|
|
}
|
|
|
|
if (mLastMutationEvent) {
|
|
NS_ASSERTION(!mLastMutationEvent->NextEvent(),
|
|
"why isn't the last event the end?");
|
|
mLastMutationEvent->SetNextEvent(aEvent);
|
|
}
|
|
|
|
aEvent->SetPrevEvent(mLastMutationEvent);
|
|
mLastMutationEvent = aEvent;
|
|
mMutationMap.PutEvent(aEvent);
|
|
|
|
// Because we could be hiding the target of a show event we need to get rid
|
|
// of any such events. It may be possible to do less than coallesce all
|
|
// events, however that is easiest.
|
|
if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE) {
|
|
CoalesceMutationEvents();
|
|
|
|
// mLastMutationEvent will point to something other than aEvent if and only
|
|
// if aEvent was just coalesced away. In that case a parent accessible
|
|
// must already have the required reorder and text change events so we are
|
|
// done here.
|
|
if (mLastMutationEvent != aEvent) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// We need to fire a reorder event after all of the events targeted at shown
|
|
// or hidden children of a container. So either queue a new one, or move an
|
|
// existing one to the end of the queue if the container already has a
|
|
// reorder event.
|
|
Accessible* target = aEvent->GetAccessible();
|
|
Accessible* container = aEvent->GetAccessible()->Parent();
|
|
RefPtr<AccReorderEvent> reorder;
|
|
if (!container->ReorderEventTarget()) {
|
|
reorder = new AccReorderEvent(container);
|
|
container->SetReorderEventTarget(true);
|
|
mMutationMap.PutEvent(reorder);
|
|
|
|
// Since this is the first child of container that is changing, the name of
|
|
// container may be changing.
|
|
QueueNameChange(target);
|
|
} else {
|
|
AccReorderEvent* event = downcast_accEvent(
|
|
mMutationMap.GetEvent(container, EventMap::ReorderEvent));
|
|
reorder = event;
|
|
if (mFirstMutationEvent == event) {
|
|
mFirstMutationEvent = event->NextEvent();
|
|
} else {
|
|
event->PrevEvent()->SetNextEvent(event->NextEvent());
|
|
}
|
|
|
|
event->NextEvent()->SetPrevEvent(event->PrevEvent());
|
|
event->SetNextEvent(nullptr);
|
|
}
|
|
|
|
reorder->SetEventGeneration(mEventGeneration);
|
|
reorder->SetPrevEvent(mLastMutationEvent);
|
|
mLastMutationEvent->SetNextEvent(reorder);
|
|
mLastMutationEvent = reorder;
|
|
|
|
// It is not possible to have a text change event for something other than a
|
|
// hyper text accessible.
|
|
// If the accessible doesn't have a frame, we are probably mid frame
|
|
// destruction bail early.
|
|
if (!container->IsHyperText() || !container->GetFrame()) {
|
|
return true;
|
|
}
|
|
|
|
MOZ_ASSERT(mutEvent);
|
|
|
|
nsString text;
|
|
aEvent->GetAccessible()->AppendTextTo(text);
|
|
if (text.IsEmpty()) {
|
|
return true;
|
|
}
|
|
|
|
int32_t offset = container->AsHyperText()->GetChildOffset(target);
|
|
AccTreeMutationEvent* prevEvent = aEvent->PrevEvent();
|
|
while (prevEvent &&
|
|
prevEvent->GetEventType() == nsIAccessibleEvent::EVENT_REORDER) {
|
|
prevEvent = prevEvent->PrevEvent();
|
|
}
|
|
|
|
if (prevEvent &&
|
|
prevEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE &&
|
|
mutEvent->IsHide()) {
|
|
AccHideEvent* prevHide = downcast_accEvent(prevEvent);
|
|
AccTextChangeEvent* prevTextChange = prevHide->mTextChangeEvent;
|
|
if (prevTextChange && prevHide->Parent() == mutEvent->Parent()) {
|
|
if (prevHide->mNextSibling == target) {
|
|
target->AppendTextTo(prevTextChange->mModifiedText);
|
|
prevHide->mTextChangeEvent.swap(mutEvent->mTextChangeEvent);
|
|
} else if (prevHide->mPrevSibling == target) {
|
|
nsString temp;
|
|
target->AppendTextTo(temp);
|
|
|
|
uint32_t extraLen = temp.Length();
|
|
temp += prevTextChange->mModifiedText;
|
|
;
|
|
prevTextChange->mModifiedText = temp;
|
|
prevTextChange->mStart -= extraLen;
|
|
prevHide->mTextChangeEvent.swap(mutEvent->mTextChangeEvent);
|
|
}
|
|
}
|
|
} else if (prevEvent && mutEvent->IsShow() &&
|
|
prevEvent->GetEventType() == nsIAccessibleEvent::EVENT_SHOW) {
|
|
AccShowEvent* prevShow = downcast_accEvent(prevEvent);
|
|
AccTextChangeEvent* prevTextChange = prevShow->mTextChangeEvent;
|
|
if (prevTextChange && prevShow->Parent() == target->Parent()) {
|
|
int32_t index = target->IndexInParent();
|
|
int32_t prevIndex = prevShow->GetAccessible()->IndexInParent();
|
|
if (prevIndex + 1 == index) {
|
|
target->AppendTextTo(prevTextChange->mModifiedText);
|
|
prevShow->mTextChangeEvent.swap(mutEvent->mTextChangeEvent);
|
|
} else if (index + 1 == prevIndex) {
|
|
nsString temp;
|
|
target->AppendTextTo(temp);
|
|
prevTextChange->mStart -= temp.Length();
|
|
temp += prevTextChange->mModifiedText;
|
|
prevTextChange->mModifiedText = temp;
|
|
prevShow->mTextChangeEvent.swap(mutEvent->mTextChangeEvent);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!mutEvent->mTextChangeEvent) {
|
|
mutEvent->mTextChangeEvent = new AccTextChangeEvent(
|
|
container, offset, text, mutEvent->IsShow(),
|
|
aEvent->mIsFromUserInput ? eFromUserInput : eNoUserInput);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void NotificationController::DropMutationEvent(AccTreeMutationEvent* aEvent) {
|
|
// unset the event bits since the event isn't being fired any more.
|
|
if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_REORDER) {
|
|
aEvent->GetAccessible()->SetReorderEventTarget(false);
|
|
} else if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_SHOW) {
|
|
aEvent->GetAccessible()->SetShowEventTarget(false);
|
|
} else {
|
|
aEvent->GetAccessible()->SetHideEventTarget(false);
|
|
|
|
AccHideEvent* hideEvent = downcast_accEvent(aEvent);
|
|
MOZ_ASSERT(hideEvent);
|
|
|
|
if (hideEvent->NeedsShutdown()) {
|
|
mDocument->ShutdownChildrenInSubtree(aEvent->GetAccessible());
|
|
}
|
|
}
|
|
|
|
// Do the work to splice the event out of the list.
|
|
if (mFirstMutationEvent == aEvent) {
|
|
mFirstMutationEvent = aEvent->NextEvent();
|
|
} else {
|
|
aEvent->PrevEvent()->SetNextEvent(aEvent->NextEvent());
|
|
}
|
|
|
|
if (mLastMutationEvent == aEvent) {
|
|
mLastMutationEvent = aEvent->PrevEvent();
|
|
} else {
|
|
aEvent->NextEvent()->SetPrevEvent(aEvent->PrevEvent());
|
|
}
|
|
|
|
aEvent->SetPrevEvent(nullptr);
|
|
aEvent->SetNextEvent(nullptr);
|
|
mMutationMap.RemoveEvent(aEvent);
|
|
}
|
|
|
|
void NotificationController::CoalesceMutationEvents() {
|
|
AccTreeMutationEvent* event = mFirstMutationEvent;
|
|
while (event) {
|
|
AccTreeMutationEvent* nextEvent = event->NextEvent();
|
|
uint32_t eventType = event->GetEventType();
|
|
if (event->GetEventType() == nsIAccessibleEvent::EVENT_REORDER) {
|
|
Accessible* acc = event->GetAccessible();
|
|
while (acc) {
|
|
if (acc->IsDoc()) {
|
|
break;
|
|
}
|
|
|
|
// if a parent of the reorder event's target is being hidden that
|
|
// hide event's target must have a parent that is also a reorder event
|
|
// target. That means we don't need this reorder event.
|
|
if (acc->HideEventTarget()) {
|
|
DropMutationEvent(event);
|
|
break;
|
|
}
|
|
|
|
Accessible* parent = acc->Parent();
|
|
if (parent->ReorderEventTarget()) {
|
|
AccReorderEvent* reorder = downcast_accEvent(
|
|
mMutationMap.GetEvent(parent, EventMap::ReorderEvent));
|
|
|
|
// We want to make sure that a reorder event comes after any show or
|
|
// hide events targeted at the children of its target. We keep the
|
|
// invariant that event generation goes up as you are farther in the
|
|
// queue, so we want to use the spot of the event with the higher
|
|
// generation number, and keep that generation number.
|
|
if (reorder &&
|
|
reorder->EventGeneration() < event->EventGeneration()) {
|
|
reorder->SetEventGeneration(event->EventGeneration());
|
|
|
|
// It may be true that reorder was before event, and we coalesced
|
|
// away all the show / hide events between them. In that case
|
|
// event is already immediately after reorder in the queue and we
|
|
// do not need to rearrange the list of events.
|
|
if (event != reorder->NextEvent()) {
|
|
// There really should be a show or hide event before the first
|
|
// reorder event.
|
|
if (reorder->PrevEvent()) {
|
|
reorder->PrevEvent()->SetNextEvent(reorder->NextEvent());
|
|
} else {
|
|
mFirstMutationEvent = reorder->NextEvent();
|
|
}
|
|
|
|
reorder->NextEvent()->SetPrevEvent(reorder->PrevEvent());
|
|
event->PrevEvent()->SetNextEvent(reorder);
|
|
reorder->SetPrevEvent(event->PrevEvent());
|
|
event->SetPrevEvent(reorder);
|
|
reorder->SetNextEvent(event);
|
|
}
|
|
}
|
|
DropMutationEvent(event);
|
|
break;
|
|
}
|
|
|
|
acc = parent;
|
|
}
|
|
} else if (eventType == nsIAccessibleEvent::EVENT_SHOW) {
|
|
Accessible* parent = event->GetAccessible()->Parent();
|
|
while (parent) {
|
|
if (parent->IsDoc()) {
|
|
break;
|
|
}
|
|
|
|
// if the parent of a show event is being either shown or hidden then
|
|
// we don't need to fire a show event for a subtree of that change.
|
|
if (parent->ShowEventTarget() || parent->HideEventTarget()) {
|
|
DropMutationEvent(event);
|
|
break;
|
|
}
|
|
|
|
parent = parent->Parent();
|
|
}
|
|
} else {
|
|
MOZ_ASSERT(eventType == nsIAccessibleEvent::EVENT_HIDE,
|
|
"mutation event list has an invalid event");
|
|
|
|
AccHideEvent* hideEvent = downcast_accEvent(event);
|
|
Accessible* parent = hideEvent->Parent();
|
|
while (parent) {
|
|
if (parent->IsDoc()) {
|
|
break;
|
|
}
|
|
|
|
if (parent->HideEventTarget()) {
|
|
DropMutationEvent(event);
|
|
break;
|
|
}
|
|
|
|
if (parent->ShowEventTarget()) {
|
|
AccShowEvent* showEvent = downcast_accEvent(
|
|
mMutationMap.GetEvent(parent, EventMap::ShowEvent));
|
|
if (showEvent->EventGeneration() < hideEvent->EventGeneration()) {
|
|
DropMutationEvent(hideEvent);
|
|
break;
|
|
}
|
|
}
|
|
|
|
parent = parent->Parent();
|
|
}
|
|
}
|
|
|
|
event = nextEvent;
|
|
}
|
|
}
|
|
|
|
void NotificationController::ScheduleChildDocBinding(DocAccessible* aDocument) {
|
|
// Schedule child document binding to the tree.
|
|
mHangingChildDocuments.AppendElement(aDocument);
|
|
ScheduleProcessing();
|
|
}
|
|
|
|
void NotificationController::ScheduleContentInsertion(
|
|
Accessible* aContainer, nsTArray<nsCOMPtr<nsIContent>>& aInsertions) {
|
|
if (!aInsertions.IsEmpty()) {
|
|
mContentInsertions.LookupOrAdd(aContainer)->AppendElements(aInsertions);
|
|
ScheduleProcessing();
|
|
}
|
|
}
|
|
|
|
void NotificationController::ScheduleProcessing() {
|
|
// If notification flush isn't planed yet start notification flush
|
|
// asynchronously (after style and layout).
|
|
if (mObservingState == eNotObservingRefresh) {
|
|
if (mPresShell->AddRefreshObserver(this, FlushType::Display))
|
|
mObservingState = eRefreshObserving;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// NotificationCollector: protected
|
|
|
|
bool NotificationController::IsUpdatePending() {
|
|
return mPresShell->IsLayoutFlushObserver() ||
|
|
mObservingState == eRefreshProcessingForUpdate || WaitingForParent() ||
|
|
mContentInsertions.Count() != 0 || mNotifications.Length() != 0 ||
|
|
mTextHash.Count() != 0 ||
|
|
!mDocument->HasLoadState(DocAccessible::eTreeConstructed);
|
|
}
|
|
|
|
bool NotificationController::WaitingForParent() {
|
|
DocAccessible* parentdoc = mDocument->ParentDocument();
|
|
if (!parentdoc) {
|
|
return false;
|
|
}
|
|
|
|
NotificationController* parent = parentdoc->mNotificationController;
|
|
if (!parent || parent == this) {
|
|
// Do not wait for nothing or ourselves
|
|
return false;
|
|
}
|
|
|
|
// Wait for parent's notifications processing
|
|
return parent->mContentInsertions.Count() != 0 ||
|
|
parent->mNotifications.Length() != 0;
|
|
}
|
|
|
|
void NotificationController::ProcessMutationEvents() {
|
|
// there is no reason to fire a hide event for a child of a show event
|
|
// target. That can happen if something is inserted into the tree and
|
|
// removed before the next refresh driver tick, but it should not be
|
|
// observable outside gecko so it should be safe to coalesce away any such
|
|
// events. This means that it should be fine to fire all of the hide events
|
|
// first, and then deal with any shown subtrees.
|
|
for (AccTreeMutationEvent* event = mFirstMutationEvent; event;
|
|
event = event->NextEvent()) {
|
|
if (event->GetEventType() != nsIAccessibleEvent::EVENT_HIDE) {
|
|
continue;
|
|
}
|
|
|
|
nsEventShell::FireEvent(event);
|
|
if (!mDocument) {
|
|
return;
|
|
}
|
|
|
|
AccMutationEvent* mutEvent = downcast_accEvent(event);
|
|
if (mutEvent->mTextChangeEvent) {
|
|
nsEventShell::FireEvent(mutEvent->mTextChangeEvent);
|
|
if (!mDocument) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Fire menupopup end event before a hide event if a menu goes away.
|
|
|
|
// XXX: We don't look into children of hidden subtree to find hiding
|
|
// menupopup (as we did prior bug 570275) because we don't do that when
|
|
// menu is showing (and that's impossible until bug 606924 is fixed).
|
|
// Nevertheless we should do this at least because layout coalesces
|
|
// the changes before our processing and we may miss some menupopup
|
|
// events. Now we just want to be consistent in content insertion/removal
|
|
// handling.
|
|
if (event->mAccessible->ARIARole() == roles::MENUPOPUP) {
|
|
nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_END,
|
|
event->mAccessible);
|
|
if (!mDocument) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
AccHideEvent* hideEvent = downcast_accEvent(event);
|
|
if (hideEvent->NeedsShutdown()) {
|
|
mDocument->ShutdownChildrenInSubtree(event->mAccessible);
|
|
}
|
|
}
|
|
|
|
// Group the show events by the parent of their target.
|
|
nsDataHashtable<nsPtrHashKey<Accessible>, nsTArray<AccTreeMutationEvent*>>
|
|
showEvents;
|
|
for (AccTreeMutationEvent* event = mFirstMutationEvent; event;
|
|
event = event->NextEvent()) {
|
|
if (event->GetEventType() != nsIAccessibleEvent::EVENT_SHOW) {
|
|
continue;
|
|
}
|
|
|
|
Accessible* parent = event->GetAccessible()->Parent();
|
|
showEvents.GetOrInsert(parent).AppendElement(event);
|
|
}
|
|
|
|
// We need to fire show events for the children of an accessible in the order
|
|
// of their indices at this point. So sort each set of events for the same
|
|
// container by the index of their target.
|
|
for (auto iter = showEvents.Iter(); !iter.Done(); iter.Next()) {
|
|
struct AccIdxComparator {
|
|
bool LessThan(const AccTreeMutationEvent* a,
|
|
const AccTreeMutationEvent* b) const {
|
|
int32_t aIdx = a->GetAccessible()->IndexInParent();
|
|
int32_t bIdx = b->GetAccessible()->IndexInParent();
|
|
MOZ_ASSERT(aIdx >= 0 && bIdx >= 0 && aIdx != bIdx);
|
|
return aIdx < bIdx;
|
|
}
|
|
bool Equals(const AccTreeMutationEvent* a,
|
|
const AccTreeMutationEvent* b) const {
|
|
DebugOnly<int32_t> aIdx = a->GetAccessible()->IndexInParent();
|
|
DebugOnly<int32_t> bIdx = b->GetAccessible()->IndexInParent();
|
|
MOZ_ASSERT(aIdx >= 0 && bIdx >= 0 && aIdx != bIdx);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
nsTArray<AccTreeMutationEvent*>& events = iter.Data();
|
|
events.Sort(AccIdxComparator());
|
|
for (AccTreeMutationEvent* event : events) {
|
|
nsEventShell::FireEvent(event);
|
|
if (!mDocument) {
|
|
return;
|
|
}
|
|
|
|
AccMutationEvent* mutEvent = downcast_accEvent(event);
|
|
if (mutEvent->mTextChangeEvent) {
|
|
nsEventShell::FireEvent(mutEvent->mTextChangeEvent);
|
|
if (!mDocument) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now we can fire the reorder events after all the show and hide events.
|
|
for (AccTreeMutationEvent* event = mFirstMutationEvent; event;
|
|
event = event->NextEvent()) {
|
|
if (event->GetEventType() != nsIAccessibleEvent::EVENT_REORDER) {
|
|
continue;
|
|
}
|
|
|
|
nsEventShell::FireEvent(event);
|
|
if (!mDocument) {
|
|
return;
|
|
}
|
|
|
|
Accessible* target = event->GetAccessible();
|
|
target->Document()->MaybeNotifyOfValueChange(target);
|
|
if (!mDocument) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// NotificationCollector: private
|
|
|
|
void NotificationController::WillRefresh(mozilla::TimeStamp aTime) {
|
|
Telemetry::AutoTimer<Telemetry::A11Y_TREE_UPDATE_TIMING_MS> timer;
|
|
|
|
AUTO_PROFILER_LABEL("NotificationController::WillRefresh", OTHER);
|
|
|
|
// If the document accessible that notification collector was created for is
|
|
// now shut down, don't process notifications anymore.
|
|
NS_ASSERTION(
|
|
mDocument,
|
|
"The document was shut down while refresh observer is attached!");
|
|
if (!mDocument) return;
|
|
|
|
// Wait until an update, we have started, or an interruptible reflow is
|
|
// finished.
|
|
if (mObservingState == eRefreshProcessing ||
|
|
mObservingState == eRefreshProcessingForUpdate ||
|
|
mPresShell->IsReflowInterrupted()) {
|
|
return;
|
|
}
|
|
|
|
// Process parent's notifications before ours, to get proper ordering between
|
|
// e.g. tab event and content event.
|
|
if (WaitingForParent()) {
|
|
mDocument->ParentDocument()->mNotificationController->WillRefresh(aTime);
|
|
if (!mDocument) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Any generic notifications should be queued if we're processing content
|
|
// insertions or generic notifications.
|
|
mObservingState = eRefreshProcessingForUpdate;
|
|
|
|
// Initial accessible tree construction.
|
|
if (!mDocument->HasLoadState(DocAccessible::eTreeConstructed)) {
|
|
// If document is not bound to parent at this point then the document is not
|
|
// ready yet (process notifications later).
|
|
if (!mDocument->IsBoundToParent()) {
|
|
mObservingState = eRefreshObserving;
|
|
return;
|
|
}
|
|
|
|
#ifdef A11Y_LOG
|
|
if (logging::IsEnabled(logging::eTree)) {
|
|
logging::MsgBegin("TREE", "initial tree created");
|
|
logging::Address("document", mDocument);
|
|
logging::MsgEnd();
|
|
}
|
|
#endif
|
|
|
|
mDocument->DoInitialUpdate();
|
|
|
|
NS_ASSERTION(mContentInsertions.Count() == 0,
|
|
"Pending content insertions while initial accessible tree "
|
|
"isn't created!");
|
|
}
|
|
|
|
// Initialize scroll support if needed.
|
|
if (!(mDocument->mDocFlags & DocAccessible::eScrollInitialized))
|
|
mDocument->AddScrollListener();
|
|
|
|
// Process rendered text change notifications.
|
|
for (auto iter = mTextHash.Iter(); !iter.Done(); iter.Next()) {
|
|
nsCOMPtrHashKey<nsIContent>* entry = iter.Get();
|
|
nsIContent* textNode = entry->GetKey();
|
|
Accessible* textAcc = mDocument->GetAccessible(textNode);
|
|
|
|
// If the text node is not in tree or doesn't have a frame, or placed in
|
|
// another document, then this case should have been handled already by
|
|
// content removal notifications.
|
|
nsINode* containerNode = textNode->GetFlattenedTreeParentNode();
|
|
if (!containerNode || textNode->OwnerDoc() != mDocument->DocumentNode()) {
|
|
MOZ_ASSERT(!textAcc,
|
|
"Text node was removed but accessible is kept alive!");
|
|
continue;
|
|
}
|
|
|
|
nsIFrame* textFrame = textNode->GetPrimaryFrame();
|
|
if (!textFrame) {
|
|
MOZ_ASSERT(!textAcc,
|
|
"Text node isn't rendered but accessible is kept alive!");
|
|
continue;
|
|
}
|
|
|
|
#ifdef A11Y_LOG
|
|
nsIContent* containerElm =
|
|
containerNode->IsElement() ? containerNode->AsElement() : nullptr;
|
|
#endif
|
|
|
|
nsIFrame::RenderedText text = textFrame->GetRenderedText(
|
|
0, UINT32_MAX, nsIFrame::TextOffsetType::OffsetsInContentText,
|
|
nsIFrame::TrailingWhitespace::DontTrim);
|
|
|
|
// Remove text accessible if rendered text is empty.
|
|
if (textAcc) {
|
|
if (text.mString.IsEmpty()) {
|
|
#ifdef A11Y_LOG
|
|
if (logging::IsEnabled(logging::eTree | logging::eText)) {
|
|
logging::MsgBegin("TREE", "text node lost its content; doc: %p",
|
|
mDocument);
|
|
logging::Node("container", containerElm);
|
|
logging::Node("content", textNode);
|
|
logging::MsgEnd();
|
|
}
|
|
#endif
|
|
|
|
mDocument->ContentRemoved(textAcc);
|
|
continue;
|
|
}
|
|
|
|
// Update text of the accessible and fire text change events.
|
|
#ifdef A11Y_LOG
|
|
if (logging::IsEnabled(logging::eText)) {
|
|
logging::MsgBegin("TEXT", "text may be changed; doc: %p", mDocument);
|
|
logging::Node("container", containerElm);
|
|
logging::Node("content", textNode);
|
|
logging::MsgEntry(
|
|
"old text '%s'",
|
|
NS_ConvertUTF16toUTF8(textAcc->AsTextLeaf()->Text()).get());
|
|
logging::MsgEntry("new text: '%s'",
|
|
NS_ConvertUTF16toUTF8(text.mString).get());
|
|
logging::MsgEnd();
|
|
}
|
|
#endif
|
|
|
|
TextUpdater::Run(mDocument, textAcc->AsTextLeaf(), text.mString);
|
|
continue;
|
|
}
|
|
|
|
// Append an accessible if rendered text is not empty.
|
|
if (!text.mString.IsEmpty()) {
|
|
#ifdef A11Y_LOG
|
|
if (logging::IsEnabled(logging::eTree | logging::eText)) {
|
|
logging::MsgBegin("TREE", "text node gains new content; doc: %p",
|
|
mDocument);
|
|
logging::Node("container", containerElm);
|
|
logging::Node("content", textNode);
|
|
logging::MsgEnd();
|
|
}
|
|
#endif
|
|
|
|
MOZ_ASSERT(mDocument->AccessibleOrTrueContainer(containerNode),
|
|
"Text node having rendered text hasn't accessible document!");
|
|
|
|
Accessible* container =
|
|
mDocument->AccessibleOrTrueContainer(containerNode, true);
|
|
if (container) {
|
|
nsTArray<nsCOMPtr<nsIContent>>* list =
|
|
mContentInsertions.LookupOrAdd(container);
|
|
list->AppendElement(textNode);
|
|
}
|
|
}
|
|
}
|
|
mTextHash.Clear();
|
|
|
|
// Process content inserted notifications to update the tree.
|
|
for (auto iter = mContentInsertions.ConstIter(); !iter.Done(); iter.Next()) {
|
|
mDocument->ProcessContentInserted(iter.Key(), iter.UserData());
|
|
if (!mDocument) {
|
|
return;
|
|
}
|
|
}
|
|
mContentInsertions.Clear();
|
|
|
|
// Bind hanging child documents unless we are using IPC and the
|
|
// document has no IPC actor. If we fail to bind the child doc then
|
|
// shut it down.
|
|
uint32_t hangingDocCnt = mHangingChildDocuments.Length();
|
|
nsTArray<RefPtr<DocAccessible>> newChildDocs;
|
|
for (uint32_t idx = 0; idx < hangingDocCnt; idx++) {
|
|
DocAccessible* childDoc = mHangingChildDocuments[idx];
|
|
if (childDoc->IsDefunct()) continue;
|
|
|
|
if (IPCAccessibilityActive() && !mDocument->IPCDoc()) {
|
|
childDoc->Shutdown();
|
|
continue;
|
|
}
|
|
|
|
nsIContent* ownerContent =
|
|
mDocument->DocumentNode()->FindContentForSubDocument(
|
|
childDoc->DocumentNode());
|
|
if (ownerContent) {
|
|
Accessible* outerDocAcc = mDocument->GetAccessible(ownerContent);
|
|
if (outerDocAcc && outerDocAcc->AppendChild(childDoc)) {
|
|
if (mDocument->AppendChildDocument(childDoc)) {
|
|
newChildDocs.AppendElement(std::move(mHangingChildDocuments[idx]));
|
|
continue;
|
|
}
|
|
|
|
outerDocAcc->RemoveChild(childDoc);
|
|
}
|
|
|
|
// Failed to bind the child document, destroy it.
|
|
childDoc->Shutdown();
|
|
}
|
|
}
|
|
|
|
// Clear the hanging documents list, even if we didn't bind them.
|
|
mHangingChildDocuments.Clear();
|
|
MOZ_ASSERT(mDocument, "Illicit document shutdown");
|
|
if (!mDocument) {
|
|
return;
|
|
}
|
|
|
|
// If the document is ready and all its subdocuments are completely loaded
|
|
// then process the document load.
|
|
if (mDocument->HasLoadState(DocAccessible::eReady) &&
|
|
!mDocument->HasLoadState(DocAccessible::eCompletelyLoaded) &&
|
|
hangingDocCnt == 0) {
|
|
uint32_t childDocCnt = mDocument->ChildDocumentCount(), childDocIdx = 0;
|
|
for (; childDocIdx < childDocCnt; childDocIdx++) {
|
|
DocAccessible* childDoc = mDocument->GetChildDocumentAt(childDocIdx);
|
|
if (!childDoc->HasLoadState(DocAccessible::eCompletelyLoaded)) break;
|
|
}
|
|
|
|
if (childDocIdx == childDocCnt) {
|
|
mDocument->ProcessLoad();
|
|
if (!mDocument) return;
|
|
}
|
|
}
|
|
|
|
// Process invalidation list of the document after all accessible tree
|
|
// mutation is done.
|
|
mDocument->ProcessInvalidationList();
|
|
|
|
// Process relocation list.
|
|
for (uint32_t idx = 0; idx < mRelocations.Length(); idx++) {
|
|
// owner should be in a document and have na associated DOM node (docs
|
|
// sometimes don't)
|
|
if (mRelocations[idx]->IsInDocument() &&
|
|
mRelocations[idx]->HasOwnContent()) {
|
|
mDocument->DoARIAOwnsRelocation(mRelocations[idx]);
|
|
}
|
|
}
|
|
mRelocations.Clear();
|
|
|
|
// Process only currently queued generic notifications.
|
|
// These are used for processing aria-activedescendant, DOMMenuItemActive,
|
|
// etc. Therefore, they must be processed after relocations, since relocated
|
|
// subtrees might not have been created before relocation processing and the
|
|
// target might be inside a relocated subtree.
|
|
nsTArray<RefPtr<Notification>> notifications;
|
|
notifications.SwapElements(mNotifications);
|
|
|
|
uint32_t notificationCount = notifications.Length();
|
|
for (uint32_t idx = 0; idx < notificationCount; idx++) {
|
|
notifications[idx]->Process();
|
|
if (!mDocument) return;
|
|
}
|
|
|
|
// If a generic notification occurs after this point then we may be allowed to
|
|
// process it synchronously. However we do not want to reenter if fireing
|
|
// events causes script to run.
|
|
mObservingState = eRefreshProcessing;
|
|
|
|
CoalesceMutationEvents();
|
|
ProcessMutationEvents();
|
|
mEventGeneration = 0;
|
|
|
|
// Now that we are done with them get rid of the events we fired.
|
|
RefPtr<AccTreeMutationEvent> mutEvent = std::move(mFirstMutationEvent);
|
|
mLastMutationEvent = nullptr;
|
|
mFirstMutationEvent = nullptr;
|
|
while (mutEvent) {
|
|
RefPtr<AccTreeMutationEvent> nextEvent = mutEvent->NextEvent();
|
|
Accessible* target = mutEvent->GetAccessible();
|
|
|
|
// We need to be careful here, while it may seem that we can simply 0 all
|
|
// the pending event bits that is not true. Because accessibles may be
|
|
// reparented they may be the target of both a hide event and a show event
|
|
// at the same time.
|
|
if (mutEvent->GetEventType() == nsIAccessibleEvent::EVENT_SHOW) {
|
|
target->SetShowEventTarget(false);
|
|
}
|
|
|
|
if (mutEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE) {
|
|
target->SetHideEventTarget(false);
|
|
}
|
|
|
|
// However it is not possible for a reorder event target to also be the
|
|
// target of a show or hide, so we can just zero that.
|
|
target->SetReorderEventTarget(false);
|
|
|
|
mutEvent->SetPrevEvent(nullptr);
|
|
mutEvent->SetNextEvent(nullptr);
|
|
mMutationMap.RemoveEvent(mutEvent);
|
|
mutEvent = nextEvent;
|
|
}
|
|
|
|
ProcessEventQueue();
|
|
|
|
if (IPCAccessibilityActive()) {
|
|
size_t newDocCount = newChildDocs.Length();
|
|
for (size_t i = 0; i < newDocCount; i++) {
|
|
DocAccessible* childDoc = newChildDocs[i];
|
|
if (childDoc->IsDefunct()) {
|
|
continue;
|
|
}
|
|
|
|
Accessible* parent = childDoc->Parent();
|
|
DocAccessibleChild* parentIPCDoc = mDocument->IPCDoc();
|
|
MOZ_DIAGNOSTIC_ASSERT(parentIPCDoc);
|
|
uint64_t id = reinterpret_cast<uintptr_t>(parent->UniqueID());
|
|
MOZ_DIAGNOSTIC_ASSERT(id);
|
|
DocAccessibleChild* ipcDoc = childDoc->IPCDoc();
|
|
if (ipcDoc) {
|
|
parentIPCDoc->SendBindChildDoc(ipcDoc, id);
|
|
continue;
|
|
}
|
|
|
|
ipcDoc = new DocAccessibleChild(childDoc, parentIPCDoc->Manager());
|
|
childDoc->SetIPCDoc(ipcDoc);
|
|
|
|
#if defined(XP_WIN)
|
|
parentIPCDoc->ConstructChildDocInParentProcess(
|
|
ipcDoc, id, AccessibleWrap::GetChildIDFor(childDoc));
|
|
#else
|
|
nsCOMPtr<nsIBrowserChild> browserChild =
|
|
do_GetInterface(mDocument->DocumentNode()->GetDocShell());
|
|
if (browserChild) {
|
|
static_cast<BrowserChild*>(browserChild.get())
|
|
->SendPDocAccessibleConstructor(ipcDoc, parentIPCDoc, id, 0, 0);
|
|
ipcDoc->SendPDocAccessiblePlatformExtConstructor();
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
mObservingState = eRefreshObserving;
|
|
if (!mDocument) return;
|
|
|
|
// Stop further processing if there are no new notifications of any kind or
|
|
// events and document load is processed.
|
|
if (mContentInsertions.Count() == 0 && mNotifications.IsEmpty() &&
|
|
mEvents.IsEmpty() && mTextHash.Count() == 0 &&
|
|
mHangingChildDocuments.IsEmpty() &&
|
|
mDocument->HasLoadState(DocAccessible::eCompletelyLoaded) &&
|
|
mPresShell->RemoveRefreshObserver(this, FlushType::Display)) {
|
|
mObservingState = eNotObservingRefresh;
|
|
}
|
|
}
|
|
|
|
void NotificationController::EventMap::PutEvent(AccTreeMutationEvent* aEvent) {
|
|
EventType type = GetEventType(aEvent);
|
|
uint64_t addr = reinterpret_cast<uintptr_t>(aEvent->GetAccessible());
|
|
MOZ_ASSERT((addr & 0x3) == 0, "accessible is not 4 byte aligned");
|
|
addr |= type;
|
|
mTable.Put(addr, aEvent);
|
|
}
|
|
|
|
AccTreeMutationEvent* NotificationController::EventMap::GetEvent(
|
|
Accessible* aTarget, EventType aType) {
|
|
uint64_t addr = reinterpret_cast<uintptr_t>(aTarget);
|
|
MOZ_ASSERT((addr & 0x3) == 0, "target is not 4 byte aligned");
|
|
|
|
addr |= aType;
|
|
return mTable.GetWeak(addr);
|
|
}
|
|
|
|
void NotificationController::EventMap::RemoveEvent(
|
|
AccTreeMutationEvent* aEvent) {
|
|
EventType type = GetEventType(aEvent);
|
|
uint64_t addr = reinterpret_cast<uintptr_t>(aEvent->GetAccessible());
|
|
MOZ_ASSERT((addr & 0x3) == 0, "accessible is not 4 byte aligned");
|
|
addr |= type;
|
|
|
|
MOZ_ASSERT(mTable.GetWeak(addr) == aEvent, "mTable has the wrong event");
|
|
mTable.Remove(addr);
|
|
}
|
|
|
|
NotificationController::EventMap::EventType
|
|
NotificationController::EventMap::GetEventType(AccTreeMutationEvent* aEvent) {
|
|
switch (aEvent->GetEventType()) {
|
|
case nsIAccessibleEvent::EVENT_SHOW:
|
|
return ShowEvent;
|
|
case nsIAccessibleEvent::EVENT_HIDE:
|
|
return HideEvent;
|
|
case nsIAccessibleEvent::EVENT_REORDER:
|
|
return ReorderEvent;
|
|
default:
|
|
MOZ_ASSERT_UNREACHABLE("event has invalid type");
|
|
return ShowEvent;
|
|
}
|
|
}
|