diff options
Diffstat (limited to '')
-rw-r--r-- | accessible/base/EventQueue.cpp | 427 |
1 files changed, 427 insertions, 0 deletions
diff --git a/accessible/base/EventQueue.cpp b/accessible/base/EventQueue.cpp new file mode 100644 index 0000000000..d64b358f5e --- /dev/null +++ b/accessible/base/EventQueue.cpp @@ -0,0 +1,427 @@ +/* -*- 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 "EventQueue.h" + +#include "LocalAccessible-inl.h" +#include "nsEventShell.h" +#include "DocAccessible.h" +#include "DocAccessibleChild.h" +#include "nsTextEquivUtils.h" +#ifdef A11Y_LOG +# include "Logging.h" +#endif +#include "Relation.h" + +namespace mozilla { +namespace a11y { + +// Defines the number of selection add/remove events in the queue when they +// aren't packed into single selection within event. +const unsigned int kSelChangeCountToPack = 5; + +//////////////////////////////////////////////////////////////////////////////// +// EventQueue +//////////////////////////////////////////////////////////////////////////////// + +bool EventQueue::PushEvent(AccEvent* aEvent) { + NS_ASSERTION((aEvent->mAccessible && aEvent->mAccessible->IsApplication()) || + aEvent->Document() == mDocument, + "Queued event belongs to another document!"); + + if (aEvent->mEventType == nsIAccessibleEvent::EVENT_FOCUS) { + mFocusEvent = aEvent; + return true; + } + + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier, or change the return type to void. + mEvents.AppendElement(aEvent); + + // Filter events. + CoalesceEvents(); + + if (aEvent->mEventRule != AccEvent::eDoNotEmit && + (aEvent->mEventType == nsIAccessibleEvent::EVENT_NAME_CHANGE || + aEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_REMOVED || + aEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_INSERTED)) { + PushNameOrDescriptionChange(aEvent); + } + return true; +} + +bool EventQueue::PushNameOrDescriptionChange(AccEvent* aOrigEvent) { + // Fire name/description change event on parent or related LocalAccessible + // being labelled/described given that this event hasn't been coalesced, the + // dependent's name/description was calculated from this subtree, and the + // subtree was changed. + LocalAccessible* target = aOrigEvent->mAccessible; + // If the text of a text leaf changed without replacing the leaf, the only + // event we get is text inserted on the container. In this case, we might + // need to fire a name change event on the target itself. + const bool maybeTargetNameChanged = + (aOrigEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_REMOVED || + aOrigEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_INSERTED) && + nsTextEquivUtils::HasNameRule(target, eNameFromSubtreeRule); + const bool doName = target->HasNameDependent() || maybeTargetNameChanged; + const bool doDesc = target->HasDescriptionDependent(); + if (!doName && !doDesc) { + return false; + } + bool pushed = false; + bool nameCheckAncestor = true; + // Only continue traversing up the tree if it's possible that the parent + // LocalAccessible's name (or a LocalAccessible being labelled by this + // LocalAccessible or an ancestor) can depend on this LocalAccessible's name. + LocalAccessible* parent = target; + do { + // Test possible name dependent parent. + if (doName) { + if (nameCheckAncestor && (maybeTargetNameChanged || parent != target) && + nsTextEquivUtils::HasNameRule(parent, eNameFromSubtreeRule)) { + nsAutoString name; + ENameValueFlag nameFlag = parent->Name(name); + // If name is obtained from subtree, fire name change event. + if (nameFlag == eNameFromSubtree) { + RefPtr<AccEvent> nameChangeEvent = + new AccEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, parent); + pushed |= PushEvent(nameChangeEvent); + } + nameCheckAncestor = false; + } + + Relation rel = parent->RelationByType(RelationType::LABEL_FOR); + while (LocalAccessible* relTarget = rel.LocalNext()) { + RefPtr<AccEvent> nameChangeEvent = + new AccEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, relTarget); + pushed |= PushEvent(nameChangeEvent); + } + } + + if (doDesc) { + Relation rel = parent->RelationByType(RelationType::DESCRIPTION_FOR); + while (LocalAccessible* relTarget = rel.LocalNext()) { + RefPtr<AccEvent> descChangeEvent = new AccEvent( + nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE, relTarget); + pushed |= PushEvent(descChangeEvent); + } + } + + parent = parent->LocalParent(); + } while (parent && + nsTextEquivUtils::HasNameRule(parent, eNameFromSubtreeIfReqRule)); + + return pushed; +} + +//////////////////////////////////////////////////////////////////////////////// +// EventQueue: private + +void EventQueue::CoalesceEvents() { + NS_ASSERTION(mEvents.Length(), "There should be at least one pending event!"); + uint32_t tail = mEvents.Length() - 1; + AccEvent* tailEvent = mEvents[tail]; + + switch (tailEvent->mEventRule) { + case AccEvent::eCoalesceReorder: { + DebugOnly<LocalAccessible*> target = tailEvent->mAccessible.get(); + MOZ_ASSERT( + target->IsApplication() || target->IsOuterDoc() || + target->IsXULTree(), + "Only app or outerdoc accessible reorder events are in the queue"); + MOZ_ASSERT(tailEvent->GetEventType() == nsIAccessibleEvent::EVENT_REORDER, + "only reorder events should be queued"); + break; // case eCoalesceReorder + } + + case AccEvent::eCoalesceOfSameType: { + // Coalesce old events by newer event. + for (uint32_t index = tail - 1; index < tail; index--) { + AccEvent* accEvent = mEvents[index]; + if (accEvent->mEventType == tailEvent->mEventType && + accEvent->mEventRule == tailEvent->mEventRule) { + accEvent->mEventRule = AccEvent::eDoNotEmit; + return; + } + } + break; // case eCoalesceOfSameType + } + + case AccEvent::eCoalesceSelectionChange: { + AccSelChangeEvent* tailSelChangeEvent = downcast_accEvent(tailEvent); + for (uint32_t index = tail - 1; index < tail; index--) { + AccEvent* thisEvent = mEvents[index]; + if (thisEvent->mEventRule == tailEvent->mEventRule) { + AccSelChangeEvent* thisSelChangeEvent = downcast_accEvent(thisEvent); + + // Coalesce selection change events within same control. + if (tailSelChangeEvent->mWidget == thisSelChangeEvent->mWidget) { + CoalesceSelChangeEvents(tailSelChangeEvent, thisSelChangeEvent, + index); + return; + } + } + } + break; // eCoalesceSelectionChange + } + + case AccEvent::eCoalesceStateChange: { + // If state change event is duped then ignore previous event. If state + // change event is opposite to previous event then no event is emitted + // (accessible state wasn't changed). + for (uint32_t index = tail - 1; index < tail; index--) { + AccEvent* thisEvent = mEvents[index]; + if (thisEvent->mEventRule != AccEvent::eDoNotEmit && + thisEvent->mEventType == tailEvent->mEventType && + thisEvent->mAccessible == tailEvent->mAccessible) { + AccStateChangeEvent* thisSCEvent = downcast_accEvent(thisEvent); + AccStateChangeEvent* tailSCEvent = downcast_accEvent(tailEvent); + if (thisSCEvent->mState == tailSCEvent->mState) { + thisEvent->mEventRule = AccEvent::eDoNotEmit; + if (thisSCEvent->mIsEnabled != tailSCEvent->mIsEnabled) { + tailEvent->mEventRule = AccEvent::eDoNotEmit; + } + } + } + } + break; // eCoalesceStateChange + } + + case AccEvent::eCoalesceTextSelChange: { + // Coalesce older event by newer event for the same selection or target. + // Events for same selection may have different targets and vice versa one + // target may be pointed by different selections (for latter see + // bug 927159). + for (uint32_t index = tail - 1; index < tail; index--) { + AccEvent* thisEvent = mEvents[index]; + if (thisEvent->mEventRule != AccEvent::eDoNotEmit && + thisEvent->mEventType == tailEvent->mEventType) { + AccTextSelChangeEvent* thisTSCEvent = downcast_accEvent(thisEvent); + AccTextSelChangeEvent* tailTSCEvent = downcast_accEvent(tailEvent); + if (thisTSCEvent->mSel == tailTSCEvent->mSel || + thisEvent->mAccessible == tailEvent->mAccessible) { + thisEvent->mEventRule = AccEvent::eDoNotEmit; + } + } + } + break; // eCoalesceTextSelChange + } + + case AccEvent::eRemoveDupes: { + // Check for repeat events, coalesce newly appended event by more older + // event. + for (uint32_t index = tail - 1; index < tail; index--) { + AccEvent* accEvent = mEvents[index]; + if (accEvent->mEventType == tailEvent->mEventType && + accEvent->mEventRule == tailEvent->mEventRule && + accEvent->mAccessible == tailEvent->mAccessible) { + tailEvent->mEventRule = AccEvent::eDoNotEmit; + return; + } + } + break; // case eRemoveDupes + } + + default: + break; // case eAllowDupes, eDoNotEmit + } // switch +} + +void EventQueue::CoalesceSelChangeEvents(AccSelChangeEvent* aTailEvent, + AccSelChangeEvent* aThisEvent, + uint32_t aThisIndex) { + aTailEvent->mPreceedingCount = aThisEvent->mPreceedingCount + 1; + + // Pack all preceding events into single selection within event + // when we receive too much selection add/remove events. + if (aTailEvent->mPreceedingCount >= kSelChangeCountToPack) { + aTailEvent->mEventType = nsIAccessibleEvent::EVENT_SELECTION_WITHIN; + aTailEvent->mAccessible = aTailEvent->mWidget; + aThisEvent->mEventRule = AccEvent::eDoNotEmit; + + // Do not emit any preceding selection events for same widget if they + // weren't coalesced yet. + if (aThisEvent->mEventType != nsIAccessibleEvent::EVENT_SELECTION_WITHIN) { + for (uint32_t jdx = aThisIndex - 1; jdx < aThisIndex; jdx--) { + AccEvent* prevEvent = mEvents[jdx]; + if (prevEvent->mEventRule == aTailEvent->mEventRule) { + AccSelChangeEvent* prevSelChangeEvent = downcast_accEvent(prevEvent); + if (prevSelChangeEvent->mWidget == aTailEvent->mWidget) { + prevSelChangeEvent->mEventRule = AccEvent::eDoNotEmit; + } + } + } + } + return; + } + + // Pack sequential selection remove and selection add events into + // single selection change event. + if (aTailEvent->mPreceedingCount == 1 && + aTailEvent->mItem != aThisEvent->mItem) { + if (aTailEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd && + aThisEvent->mSelChangeType == AccSelChangeEvent::eSelectionRemove) { + aThisEvent->mEventRule = AccEvent::eDoNotEmit; + aTailEvent->mEventType = nsIAccessibleEvent::EVENT_SELECTION; + aTailEvent->mPackedEvent = aThisEvent; + return; + } + + if (aThisEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd && + aTailEvent->mSelChangeType == AccSelChangeEvent::eSelectionRemove) { + aTailEvent->mEventRule = AccEvent::eDoNotEmit; + aThisEvent->mEventType = nsIAccessibleEvent::EVENT_SELECTION; + aThisEvent->mPackedEvent = aTailEvent; + return; + } + } + + // Unpack the packed selection change event because we've got one + // more selection add/remove. + if (aThisEvent->mEventType == nsIAccessibleEvent::EVENT_SELECTION) { + if (aThisEvent->mPackedEvent) { + aThisEvent->mPackedEvent->mEventType = + aThisEvent->mPackedEvent->mSelChangeType == + AccSelChangeEvent::eSelectionAdd + ? nsIAccessibleEvent::EVENT_SELECTION_ADD + : nsIAccessibleEvent::EVENT_SELECTION_REMOVE; + + aThisEvent->mPackedEvent->mEventRule = AccEvent::eCoalesceSelectionChange; + + aThisEvent->mPackedEvent = nullptr; + } + + aThisEvent->mEventType = + aThisEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd + ? nsIAccessibleEvent::EVENT_SELECTION_ADD + : nsIAccessibleEvent::EVENT_SELECTION_REMOVE; + + return; + } + + // Convert into selection add since control has single selection but other + // selection events for this control are queued. + if (aTailEvent->mEventType == nsIAccessibleEvent::EVENT_SELECTION) { + aTailEvent->mEventType = nsIAccessibleEvent::EVENT_SELECTION_ADD; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// EventQueue: event queue + +void EventQueue::ProcessEventQueue() { + // Process only currently queued events. + const nsTArray<RefPtr<AccEvent> > events = std::move(mEvents); + nsTArray<uint64_t> selectedIDs; + nsTArray<uint64_t> unselectedIDs; + + uint32_t eventCount = events.Length(); +#ifdef A11Y_LOG + if ((eventCount > 0 || mFocusEvent) && logging::IsEnabled(logging::eEvents)) { + logging::MsgBegin("EVENTS", "events processing"); + logging::Address("document", mDocument); + logging::MsgEnd(); + } +#endif + + if (mFocusEvent) { + // Always fire a pending focus event before all other events. We do this for + // two reasons: + // 1. It prevents extraneous screen reader speech if the name, states, etc. + // of the currently focused item change at the same time as another item is + // focused. If aria-activedescendant is used, even setting + // aria-activedescendant before changing other properties results in the + // property change events being queued before the focus event because we + // process aria-activedescendant async. + // 2. It improves perceived performance. Focus changes should be reported as + // soon as possible, so clients should handle focus events before they spend + // time on anything else. + RefPtr<AccEvent> event = std::move(mFocusEvent); + if (!event->mAccessible->IsDefunct()) { + FocusMgr()->ProcessFocusEvent(event); + } + } + + for (uint32_t idx = 0; idx < eventCount; idx++) { + AccEvent* event = events[idx]; + uint32_t eventType = event->mEventType; + LocalAccessible* target = event->GetAccessible(); + if (!target || target->IsDefunct()) continue; + + // Collect select changes + if (IPCAccessibilityActive()) { + if ((event->mEventRule == AccEvent::eDoNotEmit && + (eventType == nsIAccessibleEvent::EVENT_SELECTION_ADD || + eventType == nsIAccessibleEvent::EVENT_SELECTION_REMOVE || + eventType == nsIAccessibleEvent::EVENT_SELECTION)) || + eventType == nsIAccessibleEvent::EVENT_SELECTION_WITHIN) { + // The selection even was either dropped or morphed to a + // selection-within. We need to collect the items from all these events + // and manually push their new state to the parent process. + AccSelChangeEvent* selChangeEvent = downcast_accEvent(event); + LocalAccessible* item = selChangeEvent->mItem; + if (!item->IsDefunct()) { + uint64_t itemID = + item->IsDoc() ? 0 : reinterpret_cast<uint64_t>(item->UniqueID()); + bool selected = selChangeEvent->mSelChangeType == + AccSelChangeEvent::eSelectionAdd; + if (selected) { + selectedIDs.AppendElement(itemID); + } else { + unselectedIDs.AppendElement(itemID); + } + } + } + } + + if (event->mEventRule == AccEvent::eDoNotEmit) { + continue; + } + + // Dispatch caret moved and text selection change events. + if (eventType == nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED) { + SelectionMgr()->ProcessTextSelChangeEvent(event); + continue; + } + + // Fire selected state change events in support to selection events. + if (eventType == nsIAccessibleEvent::EVENT_SELECTION_ADD) { + nsEventShell::FireEvent(event->mAccessible, states::SELECTED, true, + event->mIsFromUserInput); + + } else if (eventType == nsIAccessibleEvent::EVENT_SELECTION_REMOVE) { + nsEventShell::FireEvent(event->mAccessible, states::SELECTED, false, + event->mIsFromUserInput); + + } else if (eventType == nsIAccessibleEvent::EVENT_SELECTION) { + AccSelChangeEvent* selChangeEvent = downcast_accEvent(event); + nsEventShell::FireEvent( + event->mAccessible, states::SELECTED, + (selChangeEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd), + event->mIsFromUserInput); + + if (selChangeEvent->mPackedEvent) { + nsEventShell::FireEvent(selChangeEvent->mPackedEvent->mAccessible, + states::SELECTED, + (selChangeEvent->mPackedEvent->mSelChangeType == + AccSelChangeEvent::eSelectionAdd), + selChangeEvent->mPackedEvent->mIsFromUserInput); + } + } + + nsEventShell::FireEvent(event); + + if (!mDocument) return; + } + + if (mDocument && IPCAccessibilityActive() && + (!selectedIDs.IsEmpty() || !unselectedIDs.IsEmpty())) { + DocAccessibleChild* ipcDoc = mDocument->IPCDoc(); + ipcDoc->SendSelectedAccessiblesChanged(selectedIDs, unselectedIDs); + } +} + +} // namespace a11y +} // namespace mozilla |