1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "Accessible-inl.h"
8 #include "AccIterator.h"
9 #include "DocAccessible-inl.h"
10 #include "DocAccessibleChild.h"
11 #include "HTMLImageMapAccessible.h"
12 #include "nsAccCache.h"
13 #include "nsAccessiblePivot.h"
14 #include "nsAccUtils.h"
15 #include "nsEventShell.h"
16 #include "nsTextEquivUtils.h"
17 #include "Role.h"
18 #include "RootAccessible.h"
19 #include "TreeWalker.h"
20 #include "xpcAccessibleDocument.h"
21
22 #include "nsIMutableArray.h"
23 #include "nsICommandManager.h"
24 #include "nsIDocShell.h"
25 #include "nsIDocument.h"
26 #include "nsIDOMCharacterData.h"
27 #include "nsIDOMDocument.h"
28 #include "nsPIDOMWindow.h"
29 #include "nsIEditingSession.h"
30 #include "nsIFrame.h"
31 #include "nsIInterfaceRequestorUtils.h"
32 #include "nsImageFrame.h"
33 #include "nsIPersistentProperties2.h"
34 #include "nsIPresShell.h"
35 #include "nsIServiceManager.h"
36 #include "nsViewManager.h"
37 #include "nsIScrollableFrame.h"
38 #include "nsUnicharUtils.h"
39 #include "nsIURI.h"
40 #include "nsIWebNavigation.h"
41 #include "nsFocusManager.h"
42 #include "mozilla/ArrayUtils.h"
43 #include "mozilla/Assertions.h"
44 #include "mozilla/EventStates.h"
45 #include "mozilla/HTMLEditor.h"
46 #include "mozilla/TextEditor.h"
47 #include "mozilla/dom/TabChild.h"
48 #include "mozilla/dom/DocumentType.h"
49 #include "mozilla/dom/Element.h"
50 #include "mozilla/dom/MutationEventBinding.h"
51
52 using namespace mozilla;
53 using namespace mozilla::a11y;
54
55 ////////////////////////////////////////////////////////////////////////////////
56 // Static member initialization
57
58 static nsStaticAtom** kRelationAttrs[] = {&nsGkAtoms::aria_labelledby,
59 &nsGkAtoms::aria_describedby,
60 &nsGkAtoms::aria_details,
61 &nsGkAtoms::aria_owns,
62 &nsGkAtoms::aria_controls,
63 &nsGkAtoms::aria_flowto,
64 &nsGkAtoms::aria_errormessage,
65 &nsGkAtoms::_for,
66 &nsGkAtoms::control};
67
68 static const uint32_t kRelationAttrsLen = ArrayLength(kRelationAttrs);
69
70 ////////////////////////////////////////////////////////////////////////////////
71 // Constructor/desctructor
72
DocAccessible(nsIDocument * aDocument,nsIPresShell * aPresShell)73 DocAccessible::DocAccessible(nsIDocument* aDocument, nsIPresShell* aPresShell)
74 : // XXX don't pass a document to the Accessible constructor so that we
75 // don't set mDoc until our vtable is fully setup. If we set mDoc before
76 // setting up the vtable we will call Accessible::AddRef() but not the
77 // overrides of it for subclasses. It is important to call those
78 // overrides to avoid confusing leak checking machinary.
79 HyperTextAccessibleWrap(nullptr, nullptr),
80 // XXX aaronl should we use an algorithm for the initial cache size?
81 mAccessibleCache(kDefaultCacheLength),
82 mNodeToAccessibleMap(kDefaultCacheLength),
83 mDocumentNode(aDocument),
84 mScrollPositionChangedTicks(0),
85 mLoadState(eTreeConstructionPending),
86 mDocFlags(0),
87 mLoadEventType(0),
88 mARIAAttrOldValue{nullptr},
89 mVirtualCursor(nullptr),
90 mPresShell(aPresShell),
91 mIPCDoc(nullptr) {
92 mGenericTypes |= eDocument;
93 mStateFlags |= eNotNodeMapEntry;
94 mDoc = this;
95
96 MOZ_ASSERT(mPresShell, "should have been given a pres shell");
97 mPresShell->SetDocAccessible(this);
98
99 // If this is a XUL Document, it should not implement nsHyperText
100 if (mDocumentNode && mDocumentNode->IsXULDocument())
101 mGenericTypes &= ~eHyperText;
102 }
103
~DocAccessible()104 DocAccessible::~DocAccessible() {
105 NS_ASSERTION(!mPresShell, "LastRelease was never called!?!");
106 }
107
108 ////////////////////////////////////////////////////////////////////////////////
109 // nsISupports
110
111 NS_IMPL_CYCLE_COLLECTION_CLASS(DocAccessible)
112
113 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocAccessible, Accessible)
114 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNotificationController)
115 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVirtualCursor)
116 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildDocuments)
117 for (auto iter = tmp->mDependentIDsHash.Iter(); !iter.Done(); iter.Next()) {
118 AttrRelProviderArray* providers = iter.UserData();
119
120 for (int32_t jdx = providers->Length() - 1; jdx >= 0; jdx--) {
121 NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(
122 cb, "content of dependent ids hash entry of document accessible");
123
124 AttrRelProvider* provider = (*providers)[jdx];
125 cb.NoteXPCOMChild(provider->mContent);
126
127 NS_ASSERTION(provider->mContent->IsInUncomposedDoc(),
128 "Referred content is not in document!");
129 }
130 }
131 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAccessibleCache)
132 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnchorJumpElm)
133 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInvalidationList)
134 for (auto it = tmp->mARIAOwnsHash.ConstIter(); !it.Done(); it.Next()) {
135 nsTArray<RefPtr<Accessible> >* ar = it.UserData();
136 for (uint32_t i = 0; i < ar->Length(); i++) {
137 NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mARIAOwnsHash entry item");
138 cb.NoteXPCOMChild(ar->ElementAt(i));
139 }
140 }
141 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
142
143 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocAccessible, Accessible)
144 NS_IMPL_CYCLE_COLLECTION_UNLINK(mNotificationController)
145 NS_IMPL_CYCLE_COLLECTION_UNLINK(mVirtualCursor)
146 NS_IMPL_CYCLE_COLLECTION_UNLINK(mChildDocuments)
147 tmp->mDependentIDsHash.Clear();
148 tmp->mNodeToAccessibleMap.Clear();
149 NS_IMPL_CYCLE_COLLECTION_UNLINK(mAccessibleCache)
150 NS_IMPL_CYCLE_COLLECTION_UNLINK(mAnchorJumpElm)
151 NS_IMPL_CYCLE_COLLECTION_UNLINK(mInvalidationList)
152 tmp->mARIAOwnsHash.Clear();
153 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
154
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocAccessible)155 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocAccessible)
156 NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver)
157 NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
158 NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
159 NS_INTERFACE_MAP_ENTRY(nsIObserver)
160 NS_INTERFACE_MAP_ENTRY(nsIAccessiblePivotObserver)
161 NS_INTERFACE_MAP_END_INHERITING(HyperTextAccessible)
162
163 NS_IMPL_ADDREF_INHERITED(DocAccessible, HyperTextAccessible)
164 NS_IMPL_RELEASE_INHERITED(DocAccessible, HyperTextAccessible)
165
166 ////////////////////////////////////////////////////////////////////////////////
167 // nsIAccessible
168
169 ENameValueFlag DocAccessible::Name(nsString& aName) {
170 aName.Truncate();
171
172 if (mParent) {
173 mParent->Name(aName); // Allow owning iframe to override the name
174 }
175 if (aName.IsEmpty()) {
176 // Allow name via aria-labelledby or title attribute
177 Accessible::Name(aName);
178 }
179 if (aName.IsEmpty()) {
180 Title(aName); // Try title element
181 }
182 if (aName.IsEmpty()) { // Last resort: use URL
183 URL(aName);
184 }
185
186 return eNameOK;
187 }
188
189 // Accessible public method
NativeRole()190 role DocAccessible::NativeRole() {
191 nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mDocumentNode);
192 if (docShell) {
193 nsCOMPtr<nsIDocShellTreeItem> sameTypeRoot;
194 docShell->GetSameTypeRootTreeItem(getter_AddRefs(sameTypeRoot));
195 int32_t itemType = docShell->ItemType();
196 if (sameTypeRoot == docShell) {
197 // Root of content or chrome tree
198 if (itemType == nsIDocShellTreeItem::typeChrome)
199 return roles::CHROME_WINDOW;
200
201 if (itemType == nsIDocShellTreeItem::typeContent) {
202 #ifdef MOZ_XUL
203 if (mDocumentNode && mDocumentNode->IsXULDocument())
204 return roles::APPLICATION;
205 #endif
206 return roles::DOCUMENT;
207 }
208 } else if (itemType == nsIDocShellTreeItem::typeContent) {
209 return roles::DOCUMENT;
210 }
211 }
212
213 return roles::PANE; // Fall back;
214 }
215
Description(nsString & aDescription)216 void DocAccessible::Description(nsString& aDescription) {
217 if (mParent) mParent->Description(aDescription);
218
219 if (HasOwnContent() && aDescription.IsEmpty()) {
220 nsTextEquivUtils::GetTextEquivFromIDRefs(this, nsGkAtoms::aria_describedby,
221 aDescription);
222 }
223 }
224
225 // Accessible public method
NativeState()226 uint64_t DocAccessible::NativeState() {
227 // Document is always focusable.
228 uint64_t state =
229 states::FOCUSABLE; // keep in sync with NativeInteractiveState() impl
230 if (FocusMgr()->IsFocused(this)) state |= states::FOCUSED;
231
232 // Expose stale state until the document is ready (DOM is loaded and tree is
233 // constructed).
234 if (!HasLoadState(eReady)) state |= states::STALE;
235
236 // Expose state busy until the document and all its subdocuments is completely
237 // loaded.
238 if (!HasLoadState(eCompletelyLoaded)) state |= states::BUSY;
239
240 nsIFrame* frame = GetFrame();
241 if (!frame || !frame->IsVisibleConsideringAncestors(
242 nsIFrame::VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY)) {
243 state |= states::INVISIBLE | states::OFFSCREEN;
244 }
245
246 RefPtr<TextEditor> textEditor = GetEditor();
247 state |= textEditor ? states::EDITABLE : states::READONLY;
248
249 return state;
250 }
251
NativeInteractiveState() const252 uint64_t DocAccessible::NativeInteractiveState() const {
253 // Document is always focusable.
254 return states::FOCUSABLE;
255 }
256
NativelyUnavailable() const257 bool DocAccessible::NativelyUnavailable() const { return false; }
258
259 // Accessible public method
ApplyARIAState(uint64_t * aState) const260 void DocAccessible::ApplyARIAState(uint64_t* aState) const {
261 // Grab states from content element.
262 if (mContent) Accessible::ApplyARIAState(aState);
263
264 // Allow iframe/frame etc. to have final state override via ARIA.
265 if (mParent) mParent->ApplyARIAState(aState);
266 }
267
Attributes()268 already_AddRefed<nsIPersistentProperties> DocAccessible::Attributes() {
269 nsCOMPtr<nsIPersistentProperties> attributes =
270 HyperTextAccessibleWrap::Attributes();
271
272 // No attributes if document is not attached to the tree or if it's a root
273 // document.
274 if (!mParent || IsRoot()) return attributes.forget();
275
276 // Override ARIA object attributes from outerdoc.
277 aria::AttrIterator attribIter(mParent->GetContent());
278 nsAutoString name, value, unused;
279 while (attribIter.Next(name, value))
280 attributes->SetStringProperty(NS_ConvertUTF16toUTF8(name), value, unused);
281
282 return attributes.forget();
283 }
284
FocusedChild()285 Accessible* DocAccessible::FocusedChild() {
286 // Return an accessible for the current global focus, which does not have to
287 // be contained within the current document.
288 return FocusMgr()->FocusedAccessible();
289 }
290
TakeFocus()291 void DocAccessible::TakeFocus() {
292 // Focus the document.
293 nsFocusManager* fm = nsFocusManager::GetFocusManager();
294 nsCOMPtr<nsIDOMElement> newFocus;
295 fm->MoveFocus(mDocumentNode->GetWindow(), nullptr,
296 nsFocusManager::MOVEFOCUS_ROOT, 0, getter_AddRefs(newFocus));
297 }
298
299 // HyperTextAccessible method
GetEditor() const300 already_AddRefed<TextEditor> DocAccessible::GetEditor() const {
301 // Check if document is editable (designMode="on" case). Otherwise check if
302 // the html:body (for HTML document case) or document element is editable.
303 if (!mDocumentNode->HasFlag(NODE_IS_EDITABLE) &&
304 (!mContent || !mContent->HasFlag(NODE_IS_EDITABLE)))
305 return nullptr;
306
307 nsCOMPtr<nsIDocShell> docShell = mDocumentNode->GetDocShell();
308 if (!docShell) {
309 return nullptr;
310 }
311
312 nsCOMPtr<nsIEditingSession> editingSession;
313 docShell->GetEditingSession(getter_AddRefs(editingSession));
314 if (!editingSession) return nullptr; // No editing session interface
315
316 RefPtr<HTMLEditor> htmlEditor =
317 editingSession->GetHTMLEditorForWindow(mDocumentNode->GetWindow());
318 if (!htmlEditor) {
319 return nullptr;
320 }
321
322 bool isEditable = false;
323 htmlEditor->GetIsDocumentEditable(&isEditable);
324 if (isEditable) {
325 return htmlEditor.forget();
326 }
327
328 return nullptr;
329 }
330
331 // DocAccessible public method
332
URL(nsAString & aURL) const333 void DocAccessible::URL(nsAString& aURL) const {
334 nsCOMPtr<nsISupports> container = mDocumentNode->GetContainer();
335 nsCOMPtr<nsIWebNavigation> webNav(do_GetInterface(container));
336 nsAutoCString theURL;
337 if (webNav) {
338 nsCOMPtr<nsIURI> pURI;
339 webNav->GetCurrentURI(getter_AddRefs(pURI));
340 if (pURI) pURI->GetSpec(theURL);
341 }
342 CopyUTF8toUTF16(theURL, aURL);
343 }
344
DocType(nsAString & aType) const345 void DocAccessible::DocType(nsAString& aType) const {
346 #ifdef MOZ_XUL
347 if (mDocumentNode->IsXULDocument()) {
348 aType.AssignLiteral("window"); // doctype not implemented for XUL at time
349 // of writing - causes assertion
350 return;
351 }
352 #endif
353 dom::DocumentType* docType = mDocumentNode->GetDoctype();
354 if (docType) docType->GetPublicId(aType);
355 }
356
357 ////////////////////////////////////////////////////////////////////////////////
358 // Accessible
359
Init()360 void DocAccessible::Init() {
361 #ifdef A11Y_LOG
362 if (logging::IsEnabled(logging::eDocCreate))
363 logging::DocCreate("document initialize", mDocumentNode, this);
364 #endif
365
366 // Initialize notification controller.
367 mNotificationController = new NotificationController(this, mPresShell);
368
369 // Mark the document accessible as loaded if its DOM document was loaded at
370 // this point (this can happen because a11y is started late or DOM document
371 // having no container was loaded.
372 if (mDocumentNode->GetReadyStateEnum() == nsIDocument::READYSTATE_COMPLETE)
373 mLoadState |= eDOMLoaded;
374
375 AddEventListeners();
376 }
377
Shutdown()378 void DocAccessible::Shutdown() {
379 if (!mPresShell) // already shutdown
380 return;
381
382 #ifdef A11Y_LOG
383 if (logging::IsEnabled(logging::eDocDestroy))
384 logging::DocDestroy("document shutdown", mDocumentNode, this);
385 #endif
386
387 // Mark the document as shutdown before AT is notified about the document
388 // removal from its container (valid for root documents on ATK and due to
389 // some reason for MSAA, refer to bug 757392 for details).
390 mStateFlags |= eIsDefunct;
391
392 if (mNotificationController) {
393 mNotificationController->Shutdown();
394 mNotificationController = nullptr;
395 }
396
397 RemoveEventListeners();
398
399 if (mParent) {
400 DocAccessible* parentDocument = mParent->Document();
401 if (parentDocument) parentDocument->RemoveChildDocument(this);
402
403 mParent->RemoveChild(this);
404 }
405
406 // Walk the array backwards because child documents remove themselves from the
407 // array as they are shutdown.
408 int32_t childDocCount = mChildDocuments.Length();
409 for (int32_t idx = childDocCount - 1; idx >= 0; idx--)
410 mChildDocuments[idx]->Shutdown();
411
412 mChildDocuments.Clear();
413
414 // XXX thinking about ordering?
415 if (mIPCDoc) {
416 MOZ_ASSERT(IPCAccessibilityActive());
417 mIPCDoc->Shutdown();
418 MOZ_ASSERT(!mIPCDoc);
419 }
420
421 if (mVirtualCursor) {
422 mVirtualCursor->RemoveObserver(this);
423 mVirtualCursor = nullptr;
424 }
425
426 mPresShell->SetDocAccessible(nullptr);
427 mPresShell = nullptr; // Avoid reentrancy
428
429 mDependentIDsHash.Clear();
430 mNodeToAccessibleMap.Clear();
431
432 for (auto iter = mAccessibleCache.Iter(); !iter.Done(); iter.Next()) {
433 Accessible* accessible = iter.Data();
434 MOZ_ASSERT(accessible);
435 if (accessible && !accessible->IsDefunct()) {
436 // Unlink parent to avoid its cleaning overhead in shutdown.
437 accessible->mParent = nullptr;
438 accessible->Shutdown();
439 }
440 iter.Remove();
441 }
442
443 HyperTextAccessibleWrap::Shutdown();
444
445 GetAccService()->NotifyOfDocumentShutdown(this, mDocumentNode);
446 mDocumentNode = nullptr;
447 }
448
GetFrame() const449 nsIFrame* DocAccessible::GetFrame() const {
450 nsIFrame* root = nullptr;
451 if (mPresShell) root = mPresShell->GetRootFrame();
452
453 return root;
454 }
455
456 // DocAccessible protected member
RelativeBounds(nsIFrame ** aRelativeFrame) const457 nsRect DocAccessible::RelativeBounds(nsIFrame** aRelativeFrame) const {
458 *aRelativeFrame = GetFrame();
459
460 nsIDocument* document = mDocumentNode;
461 nsIDocument* parentDoc = nullptr;
462
463 nsRect bounds;
464 while (document) {
465 nsIPresShell* presShell = document->GetShell();
466 if (!presShell) return nsRect();
467
468 nsRect scrollPort;
469 nsIScrollableFrame* sf = presShell->GetRootScrollFrameAsScrollable();
470 if (sf) {
471 scrollPort = sf->GetScrollPortRect();
472 } else {
473 nsIFrame* rootFrame = presShell->GetRootFrame();
474 if (!rootFrame) return nsRect();
475
476 scrollPort = rootFrame->GetRect();
477 }
478
479 if (parentDoc) { // After first time thru loop
480 // XXXroc bogus code! scrollPort is relative to the viewport of
481 // this document, but we're intersecting rectangles derived from
482 // multiple documents and assuming they're all in the same coordinate
483 // system. See bug 514117.
484 bounds.IntersectRect(scrollPort, bounds);
485 } else { // First time through loop
486 bounds = scrollPort;
487 }
488
489 document = parentDoc = document->GetParentDocument();
490 }
491
492 return bounds;
493 }
494
495 // DocAccessible protected member
AddEventListeners()496 nsresult DocAccessible::AddEventListeners() {
497 nsCOMPtr<nsIDocShell> docShell(mDocumentNode->GetDocShell());
498
499 // We want to add a command observer only if the document is content and has
500 // an editor.
501 if (docShell->ItemType() == nsIDocShellTreeItem::typeContent) {
502 nsCOMPtr<nsICommandManager> commandManager = docShell->GetCommandManager();
503 if (commandManager)
504 commandManager->AddCommandObserver(this, "obs_documentCreated");
505 }
506
507 SelectionMgr()->AddDocSelectionListener(mPresShell);
508
509 // Add document observer.
510 mDocumentNode->AddObserver(this);
511 return NS_OK;
512 }
513
514 // DocAccessible protected member
RemoveEventListeners()515 nsresult DocAccessible::RemoveEventListeners() {
516 // Remove listeners associated with content documents
517 // Remove scroll position listener
518 RemoveScrollListener();
519
520 NS_ASSERTION(mDocumentNode, "No document during removal of listeners.");
521
522 if (mDocumentNode) {
523 mDocumentNode->RemoveObserver(this);
524
525 nsCOMPtr<nsIDocShell> docShell(mDocumentNode->GetDocShell());
526 NS_ASSERTION(docShell, "doc should support nsIDocShellTreeItem.");
527
528 if (docShell) {
529 if (docShell->ItemType() == nsIDocShellTreeItem::typeContent) {
530 nsCOMPtr<nsICommandManager> commandManager =
531 docShell->GetCommandManager();
532 if (commandManager) {
533 commandManager->RemoveCommandObserver(this, "obs_documentCreated");
534 }
535 }
536 }
537 }
538
539 if (mScrollWatchTimer) {
540 mScrollWatchTimer->Cancel();
541 mScrollWatchTimer = nullptr;
542 NS_RELEASE_THIS(); // Kung fu death grip
543 }
544
545 SelectionMgr()->RemoveDocSelectionListener(mPresShell);
546 return NS_OK;
547 }
548
ScrollTimerCallback(nsITimer * aTimer,void * aClosure)549 void DocAccessible::ScrollTimerCallback(nsITimer* aTimer, void* aClosure) {
550 DocAccessible* docAcc = reinterpret_cast<DocAccessible*>(aClosure);
551
552 if (docAcc && docAcc->mScrollPositionChangedTicks &&
553 ++docAcc->mScrollPositionChangedTicks > 2) {
554 // Whenever scroll position changes, mScrollPositionChangeTicks gets reset
555 // to 1 We only want to fire accessibilty scroll event when scrolling stops
556 // or pauses Therefore, we wait for no scroll events to occur between 2
557 // ticks of this timer That indicates a pause in scrolling, so we fire the
558 // accessibilty scroll event
559 nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_SCROLLING_END, docAcc);
560
561 docAcc->mScrollPositionChangedTicks = 0;
562 if (docAcc->mScrollWatchTimer) {
563 docAcc->mScrollWatchTimer->Cancel();
564 docAcc->mScrollWatchTimer = nullptr;
565 NS_RELEASE(docAcc); // Release kung fu death grip
566 }
567 }
568 }
569
570 ////////////////////////////////////////////////////////////////////////////////
571 // nsIScrollPositionListener
572
ScrollPositionDidChange(nscoord aX,nscoord aY)573 void DocAccessible::ScrollPositionDidChange(nscoord aX, nscoord aY) {
574 // Start new timer, if the timer cycles at least 1 full cycle without more
575 // scroll position changes, then the ::Notify() method will fire the
576 // accessibility event for scroll position changes
577 const uint32_t kScrollPosCheckWait = 50;
578 if (mScrollWatchTimer) {
579 mScrollWatchTimer->SetDelay(
580 kScrollPosCheckWait); // Create new timer, to avoid leaks
581 } else {
582 NS_NewTimerWithFuncCallback(getter_AddRefs(mScrollWatchTimer),
583 ScrollTimerCallback, this, kScrollPosCheckWait,
584 nsITimer::TYPE_REPEATING_SLACK,
585 "a11y::DocAccessible::ScrollPositionDidChange");
586 if (mScrollWatchTimer) {
587 NS_ADDREF_THIS(); // Kung fu death grip
588 }
589 }
590 mScrollPositionChangedTicks = 1;
591 }
592
593 ////////////////////////////////////////////////////////////////////////////////
594 // nsIObserver
595
596 NS_IMETHODIMP
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)597 DocAccessible::Observe(nsISupports* aSubject, const char* aTopic,
598 const char16_t* aData) {
599 if (!nsCRT::strcmp(aTopic, "obs_documentCreated")) {
600 // State editable will now be set, readonly is now clear
601 // Normally we only fire delayed events created from the node, not an
602 // accessible object. See the AccStateChangeEvent constructor for details
603 // about this exceptional case.
604 RefPtr<AccEvent> event =
605 new AccStateChangeEvent(this, states::EDITABLE, true);
606 FireDelayedEvent(event);
607 }
608
609 return NS_OK;
610 }
611
612 ////////////////////////////////////////////////////////////////////////////////
613 // nsIAccessiblePivotObserver
614
615 NS_IMETHODIMP
OnPivotChanged(nsIAccessiblePivot * aPivot,nsIAccessible * aOldAccessible,int32_t aOldStart,int32_t aOldEnd,PivotMoveReason aReason,bool aIsFromUserInput)616 DocAccessible::OnPivotChanged(nsIAccessiblePivot* aPivot,
617 nsIAccessible* aOldAccessible, int32_t aOldStart,
618 int32_t aOldEnd, PivotMoveReason aReason,
619 bool aIsFromUserInput) {
620 RefPtr<AccEvent> event = new AccVCChangeEvent(
621 this, (aOldAccessible ? aOldAccessible->ToInternalAccessible() : nullptr),
622 aOldStart, aOldEnd, aReason,
623 aIsFromUserInput ? eFromUserInput : eNoUserInput);
624 nsEventShell::FireEvent(event);
625
626 return NS_OK;
627 }
628
629 ////////////////////////////////////////////////////////////////////////////////
630 // nsIDocumentObserver
631
632 NS_IMPL_NSIDOCUMENTOBSERVER_CORE_STUB(DocAccessible)
NS_IMPL_NSIDOCUMENTOBSERVER_LOAD_STUB(DocAccessible)633 NS_IMPL_NSIDOCUMENTOBSERVER_LOAD_STUB(DocAccessible)
634 NS_IMPL_NSIDOCUMENTOBSERVER_STYLE_STUB(DocAccessible)
635
636 void DocAccessible::AttributeWillChange(dom::Element* aElement,
637 int32_t aNameSpaceID,
638 nsAtom* aAttribute, int32_t aModType,
639 const nsAttrValue* aNewValue) {
640 Accessible* accessible = GetAccessible(aElement);
641 if (!accessible) {
642 if (aElement != mContent) return;
643
644 accessible = this;
645 }
646
647 // Update dependent IDs cache. Take care of elements that are accessible
648 // because dependent IDs cache doesn't contain IDs from non accessible
649 // elements.
650 if (aModType != dom::MutationEventBinding::ADDITION)
651 RemoveDependentIDsFor(accessible, aAttribute);
652
653 if (aAttribute == nsGkAtoms::id) {
654 RelocateARIAOwnedIfNeeded(aElement);
655 }
656
657 // Store the ARIA attribute old value so that it can be used after
658 // attribute change. Note, we assume there's no nested ARIA attribute
659 // changes. If this happens then we should end up with keeping a stack of
660 // old values.
661
662 // XXX TODO: bugs 472142, 472143.
663 // Here we will want to cache whatever attribute values we are interested
664 // in, such as the existence of aria-pressed for button (so we know if we
665 // need to newly expose it as a toggle button) etc.
666 if (aAttribute == nsGkAtoms::aria_checked ||
667 aAttribute == nsGkAtoms::aria_pressed) {
668 mARIAAttrOldValue = (aModType != dom::MutationEventBinding::ADDITION)
669 ? nsAccUtils::GetARIAToken(aElement, aAttribute)
670 : nullptr;
671 return;
672 }
673
674 if (aAttribute == nsGkAtoms::aria_disabled ||
675 aAttribute == nsGkAtoms::disabled)
676 mStateBitWasOn = accessible->Unavailable();
677 }
678
NativeAnonymousChildListChange(nsIContent * aContent,bool aIsRemove)679 void DocAccessible::NativeAnonymousChildListChange(nsIContent* aContent,
680 bool aIsRemove) {}
681
AttributeChanged(dom::Element * aElement,int32_t aNameSpaceID,nsAtom * aAttribute,int32_t aModType,const nsAttrValue * aOldValue)682 void DocAccessible::AttributeChanged(dom::Element* aElement,
683 int32_t aNameSpaceID, nsAtom* aAttribute,
684 int32_t aModType,
685 const nsAttrValue* aOldValue) {
686 NS_ASSERTION(!IsDefunct(),
687 "Attribute changed called on defunct document accessible!");
688
689 // Proceed even if the element is not accessible because element may become
690 // accessible if it gets certain attribute.
691 if (UpdateAccessibleOnAttrChange(aElement, aAttribute)) return;
692
693 // Ignore attribute change if the element doesn't have an accessible (at all
694 // or still) iff the element is not a root content of this document accessible
695 // (which is treated as attribute change on this document accessible).
696 // Note: we don't bail if all the content hasn't finished loading because
697 // these attributes are changing for a loaded part of the content.
698 Accessible* accessible = GetAccessible(aElement);
699 if (!accessible) {
700 if (mContent != aElement) return;
701
702 accessible = this;
703 }
704
705 MOZ_ASSERT(accessible->IsBoundToParent() || accessible->IsDoc(),
706 "DOM attribute change on an accessible detached from the tree");
707
708 // Fire accessible events iff there's an accessible, otherwise we consider
709 // the accessible state wasn't changed, i.e. its state is initial state.
710 AttributeChangedImpl(accessible, aNameSpaceID, aAttribute);
711
712 // Update dependent IDs cache. Take care of accessible elements because no
713 // accessible element means either the element is not accessible at all or
714 // its accessible will be created later. It doesn't make sense to keep
715 // dependent IDs for non accessible elements. For the second case we'll update
716 // dependent IDs cache when its accessible is created.
717 if (aModType == dom::MutationEventBinding::MODIFICATION ||
718 aModType == dom::MutationEventBinding::ADDITION) {
719 AddDependentIDsFor(accessible, aAttribute);
720 }
721 }
722
723 // DocAccessible protected member
AttributeChangedImpl(Accessible * aAccessible,int32_t aNameSpaceID,nsAtom * aAttribute)724 void DocAccessible::AttributeChangedImpl(Accessible* aAccessible,
725 int32_t aNameSpaceID,
726 nsAtom* aAttribute) {
727 // Fire accessible event after short timer, because we need to wait for
728 // DOM attribute & resulting layout to actually change. Otherwise,
729 // assistive technology will retrieve the wrong state/value/selection info.
730
731 // XXX todo
732 // We still need to handle special HTML cases here
733 // For example, if an <img>'s usemap attribute is modified
734 // Otherwise it may just be a state change, for example an object changing
735 // its visibility
736 //
737 // XXX todo: report aria state changes for "undefined" literal value changes
738 // filed as bug 472142
739 //
740 // XXX todo: invalidate accessible when aria state changes affect exposed
741 // role filed as bug 472143
742
743 // Universal boolean properties that don't require a role. Fire the state
744 // change when disabled or aria-disabled attribute is set.
745 // Note. Checking the XUL or HTML namespace would not seem to gain us
746 // anything, because disabled attribute really is going to mean the same
747 // thing in any namespace.
748 // Note. We use the attribute instead of the disabled state bit because
749 // ARIA's aria-disabled does not affect the disabled state bit.
750 if (aAttribute == nsGkAtoms::disabled ||
751 aAttribute == nsGkAtoms::aria_disabled) {
752 // Do nothing if state wasn't changed (like @aria-disabled was removed but
753 // @disabled is still presented).
754 if (aAccessible->Unavailable() == mStateBitWasOn) return;
755
756 RefPtr<AccEvent> enabledChangeEvent =
757 new AccStateChangeEvent(aAccessible, states::ENABLED, mStateBitWasOn);
758 FireDelayedEvent(enabledChangeEvent);
759
760 RefPtr<AccEvent> sensitiveChangeEvent =
761 new AccStateChangeEvent(aAccessible, states::SENSITIVE, mStateBitWasOn);
762 FireDelayedEvent(sensitiveChangeEvent);
763 return;
764 }
765
766 // Check for namespaced ARIA attribute
767 if (aNameSpaceID == kNameSpaceID_None) {
768 // Check for hyphenated aria-foo property?
769 if (StringBeginsWith(nsDependentAtomString(aAttribute),
770 NS_LITERAL_STRING("aria-"))) {
771 ARIAAttributeChanged(aAccessible, aAttribute);
772 }
773 }
774
775 // Fire name change and description change events. XXX: it's not complete and
776 // dupes the code logic of accessible name and description calculation, we do
777 // that for performance reasons.
778 if (aAttribute == nsGkAtoms::aria_label) {
779 FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, aAccessible);
780 return;
781 }
782
783 if (aAttribute == nsGkAtoms::aria_describedby) {
784 FireDelayedEvent(nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE, aAccessible);
785 return;
786 }
787
788 dom::Element* elm = aAccessible->GetContent()->AsElement();
789 if (aAttribute == nsGkAtoms::aria_labelledby &&
790 !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_label)) {
791 FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, aAccessible);
792 return;
793 }
794
795 if (aAttribute == nsGkAtoms::alt &&
796 !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_label) &&
797 !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_labelledby)) {
798 FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, aAccessible);
799 return;
800 }
801
802 if (aAttribute == nsGkAtoms::title) {
803 if (!elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_label) &&
804 !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_labelledby) &&
805 !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::alt)) {
806 FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, aAccessible);
807 return;
808 }
809
810 if (!elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_describedby))
811 FireDelayedEvent(nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE,
812 aAccessible);
813
814 return;
815 }
816
817 if (aAttribute == nsGkAtoms::aria_busy) {
818 bool isOn = elm->AttrValueIs(aNameSpaceID, aAttribute, nsGkAtoms::_true,
819 eCaseMatters);
820 RefPtr<AccEvent> event =
821 new AccStateChangeEvent(aAccessible, states::BUSY, isOn);
822 FireDelayedEvent(event);
823 return;
824 }
825
826 if (aAttribute == nsGkAtoms::id) {
827 RelocateARIAOwnedIfNeeded(elm);
828 }
829
830 // ARIA or XUL selection
831 if ((aAccessible->GetContent()->IsXULElement() &&
832 aAttribute == nsGkAtoms::selected) ||
833 aAttribute == nsGkAtoms::aria_selected) {
834 Accessible* widget =
835 nsAccUtils::GetSelectableContainer(aAccessible, aAccessible->State());
836 if (widget) {
837 AccSelChangeEvent::SelChangeType selChangeType =
838 elm->AttrValueIs(aNameSpaceID, aAttribute, nsGkAtoms::_true,
839 eCaseMatters)
840 ? AccSelChangeEvent::eSelectionAdd
841 : AccSelChangeEvent::eSelectionRemove;
842
843 RefPtr<AccEvent> event =
844 new AccSelChangeEvent(widget, aAccessible, selChangeType);
845 FireDelayedEvent(event);
846 }
847
848 return;
849 }
850
851 if (aAttribute == nsGkAtoms::contenteditable) {
852 RefPtr<AccEvent> editableChangeEvent =
853 new AccStateChangeEvent(aAccessible, states::EDITABLE);
854 FireDelayedEvent(editableChangeEvent);
855 return;
856 }
857
858 if (aAttribute == nsGkAtoms::value) {
859 if (aAccessible->IsProgress())
860 FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, aAccessible);
861 }
862 }
863
864 // DocAccessible protected member
ARIAAttributeChanged(Accessible * aAccessible,nsAtom * aAttribute)865 void DocAccessible::ARIAAttributeChanged(Accessible* aAccessible,
866 nsAtom* aAttribute) {
867 // Note: For universal/global ARIA states and properties we don't care if
868 // there is an ARIA role present or not.
869
870 if (aAttribute == nsGkAtoms::aria_required) {
871 RefPtr<AccEvent> event =
872 new AccStateChangeEvent(aAccessible, states::REQUIRED);
873 FireDelayedEvent(event);
874 return;
875 }
876
877 if (aAttribute == nsGkAtoms::aria_invalid) {
878 RefPtr<AccEvent> event =
879 new AccStateChangeEvent(aAccessible, states::INVALID);
880 FireDelayedEvent(event);
881 return;
882 }
883
884 // The activedescendant universal property redirects accessible focus events
885 // to the element with the id that activedescendant points to. Make sure
886 // the tree up to date before processing.
887 if (aAttribute == nsGkAtoms::aria_activedescendant) {
888 mNotificationController->HandleNotification<DocAccessible, Accessible>(
889 this, &DocAccessible::ARIAActiveDescendantChanged, aAccessible);
890
891 return;
892 }
893
894 // We treat aria-expanded as a global ARIA state for historical reasons
895 if (aAttribute == nsGkAtoms::aria_expanded) {
896 RefPtr<AccEvent> event =
897 new AccStateChangeEvent(aAccessible, states::EXPANDED);
898 FireDelayedEvent(event);
899 return;
900 }
901
902 // For aria attributes like drag and drop changes we fire a generic attribute
903 // change event; at least until native API comes up with a more meaningful
904 // event.
905 uint8_t attrFlags = aria::AttrCharacteristicsFor(aAttribute);
906 if (!(attrFlags & ATTR_BYPASSOBJ)) {
907 RefPtr<AccEvent> event =
908 new AccObjectAttrChangedEvent(aAccessible, aAttribute);
909 FireDelayedEvent(event);
910 }
911
912 dom::Element* elm = aAccessible->GetContent()->AsElement();
913
914 // Update aria-hidden flag for the whole subtree iff aria-hidden is changed
915 // on the root, i.e. ignore any affiliated aria-hidden changes in the subtree
916 // of top aria-hidden.
917 if (aAttribute == nsGkAtoms::aria_hidden) {
918 bool isDefined = aria::HasDefinedARIAHidden(elm);
919 if (isDefined != aAccessible->IsARIAHidden() &&
920 (!aAccessible->Parent() || !aAccessible->Parent()->IsARIAHidden())) {
921 aAccessible->SetARIAHidden(isDefined);
922
923 RefPtr<AccEvent> event =
924 new AccObjectAttrChangedEvent(aAccessible, aAttribute);
925 FireDelayedEvent(event);
926 }
927 return;
928 }
929
930 if (aAttribute == nsGkAtoms::aria_checked ||
931 (aAccessible->IsButton() && aAttribute == nsGkAtoms::aria_pressed)) {
932 const uint64_t kState = (aAttribute == nsGkAtoms::aria_checked)
933 ? states::CHECKED
934 : states::PRESSED;
935 RefPtr<AccEvent> event = new AccStateChangeEvent(aAccessible, kState);
936 FireDelayedEvent(event);
937
938 bool wasMixed = (mARIAAttrOldValue == nsGkAtoms::mixed);
939 bool isMixed = elm->AttrValueIs(kNameSpaceID_None, aAttribute,
940 nsGkAtoms::mixed, eCaseMatters);
941 if (isMixed != wasMixed) {
942 RefPtr<AccEvent> event =
943 new AccStateChangeEvent(aAccessible, states::MIXED, isMixed);
944 FireDelayedEvent(event);
945 }
946 return;
947 }
948
949 if (aAttribute == nsGkAtoms::aria_readonly) {
950 RefPtr<AccEvent> event =
951 new AccStateChangeEvent(aAccessible, states::READONLY);
952 FireDelayedEvent(event);
953 return;
954 }
955
956 // Fire text value change event whenever aria-valuetext is changed.
957 if (aAttribute == nsGkAtoms::aria_valuetext) {
958 FireDelayedEvent(nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE, aAccessible);
959 return;
960 }
961
962 // Fire numeric value change event when aria-valuenow is changed and
963 // aria-valuetext is empty
964 if (aAttribute == nsGkAtoms::aria_valuenow &&
965 (!elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_valuetext) ||
966 elm->AttrValueIs(kNameSpaceID_None, nsGkAtoms::aria_valuetext,
967 nsGkAtoms::_empty, eCaseMatters))) {
968 FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, aAccessible);
969 return;
970 }
971
972 if (aAttribute == nsGkAtoms::aria_current) {
973 RefPtr<AccEvent> event =
974 new AccStateChangeEvent(aAccessible, states::CURRENT);
975 FireDelayedEvent(event);
976 return;
977 }
978
979 if (aAttribute == nsGkAtoms::aria_owns) {
980 mNotificationController->ScheduleRelocation(aAccessible);
981 }
982 }
983
ARIAActiveDescendantChanged(Accessible * aAccessible)984 void DocAccessible::ARIAActiveDescendantChanged(Accessible* aAccessible) {
985 nsIContent* elm = aAccessible->GetContent();
986 if (elm && elm->IsElement() && aAccessible->IsActiveWidget()) {
987 nsAutoString id;
988 if (elm->AsElement()->GetAttr(kNameSpaceID_None,
989 nsGkAtoms::aria_activedescendant, id)) {
990 dom::Element* activeDescendantElm = elm->OwnerDoc()->GetElementById(id);
991 if (activeDescendantElm) {
992 Accessible* activeDescendant = GetAccessible(activeDescendantElm);
993 if (activeDescendant) {
994 FocusMgr()->ActiveItemChanged(activeDescendant, false);
995 #ifdef A11Y_LOG
996 if (logging::IsEnabled(logging::eFocus))
997 logging::ActiveItemChangeCausedBy("ARIA activedescedant changed",
998 activeDescendant);
999 #endif
1000 }
1001 }
1002 }
1003 }
1004 }
1005
ContentAppended(nsIContent * aFirstNewContent)1006 void DocAccessible::ContentAppended(nsIContent* aFirstNewContent) {}
1007
ContentStateChanged(nsIDocument * aDocument,nsIContent * aContent,EventStates aStateMask)1008 void DocAccessible::ContentStateChanged(nsIDocument* aDocument,
1009 nsIContent* aContent,
1010 EventStates aStateMask) {
1011 Accessible* accessible = GetAccessible(aContent);
1012 if (!accessible) return;
1013
1014 if (aStateMask.HasState(NS_EVENT_STATE_CHECKED)) {
1015 Accessible* widget = accessible->ContainerWidget();
1016 if (widget && widget->IsSelect()) {
1017 AccSelChangeEvent::SelChangeType selChangeType =
1018 aContent->AsElement()->State().HasState(NS_EVENT_STATE_CHECKED)
1019 ? AccSelChangeEvent::eSelectionAdd
1020 : AccSelChangeEvent::eSelectionRemove;
1021 RefPtr<AccEvent> event =
1022 new AccSelChangeEvent(widget, accessible, selChangeType);
1023 FireDelayedEvent(event);
1024 return;
1025 }
1026
1027 RefPtr<AccEvent> event = new AccStateChangeEvent(
1028 accessible, states::CHECKED,
1029 aContent->AsElement()->State().HasState(NS_EVENT_STATE_CHECKED));
1030 FireDelayedEvent(event);
1031 }
1032
1033 if (aStateMask.HasState(NS_EVENT_STATE_INVALID)) {
1034 RefPtr<AccEvent> event =
1035 new AccStateChangeEvent(accessible, states::INVALID, true);
1036 FireDelayedEvent(event);
1037 }
1038
1039 if (aStateMask.HasState(NS_EVENT_STATE_VISITED)) {
1040 RefPtr<AccEvent> event =
1041 new AccStateChangeEvent(accessible, states::TRAVERSED, true);
1042 FireDelayedEvent(event);
1043 }
1044 }
1045
DocumentStatesChanged(nsIDocument * aDocument,EventStates aStateMask)1046 void DocAccessible::DocumentStatesChanged(nsIDocument* aDocument,
1047 EventStates aStateMask) {}
1048
CharacterDataWillChange(nsIContent * aContent,const CharacterDataChangeInfo &)1049 void DocAccessible::CharacterDataWillChange(nsIContent* aContent,
1050 const CharacterDataChangeInfo&) {}
1051
CharacterDataChanged(nsIContent * aContent,const CharacterDataChangeInfo &)1052 void DocAccessible::CharacterDataChanged(nsIContent* aContent,
1053 const CharacterDataChangeInfo&) {}
1054
ContentInserted(nsIContent * aChild)1055 void DocAccessible::ContentInserted(nsIContent* aChild) {}
1056
ContentRemoved(nsIContent * aChildNode,nsIContent * aPreviousSiblingNode)1057 void DocAccessible::ContentRemoved(nsIContent* aChildNode,
1058 nsIContent* aPreviousSiblingNode) {
1059 #ifdef A11Y_LOG
1060 if (logging::IsEnabled(logging::eTree)) {
1061 logging::MsgBegin("TREE", "DOM content removed; doc: %p", this);
1062 logging::Node("container node", aChildNode->GetParent());
1063 logging::Node("content node", aChildNode);
1064 logging::MsgEnd();
1065 }
1066 #endif
1067 // This one and content removal notification from layout may result in
1068 // double processing of same subtrees. If it pops up in profiling, then
1069 // consider reusing a document node cache to reject these notifications early.
1070 ContentRemoved(aChildNode);
1071 }
1072
ParentChainChanged(nsIContent * aContent)1073 void DocAccessible::ParentChainChanged(nsIContent* aContent) {}
1074
1075 ////////////////////////////////////////////////////////////////////////////////
1076 // Accessible
1077
1078 #ifdef A11Y_LOG
HandleAccEvent(AccEvent * aEvent)1079 nsresult DocAccessible::HandleAccEvent(AccEvent* aEvent) {
1080 if (logging::IsEnabled(logging::eDocLoad))
1081 logging::DocLoadEventHandled(aEvent);
1082
1083 return HyperTextAccessible::HandleAccEvent(aEvent);
1084 }
1085 #endif
1086
1087 ////////////////////////////////////////////////////////////////////////////////
1088 // Public members
1089
GetNativeWindow() const1090 void* DocAccessible::GetNativeWindow() const {
1091 if (!mPresShell) return nullptr;
1092
1093 nsViewManager* vm = mPresShell->GetViewManager();
1094 if (!vm) return nullptr;
1095
1096 nsCOMPtr<nsIWidget> widget;
1097 vm->GetRootWidget(getter_AddRefs(widget));
1098 if (widget) return widget->GetNativeData(NS_NATIVE_WINDOW);
1099
1100 return nullptr;
1101 }
1102
GetAccessibleByUniqueIDInSubtree(void * aUniqueID)1103 Accessible* DocAccessible::GetAccessibleByUniqueIDInSubtree(void* aUniqueID) {
1104 Accessible* child = GetAccessibleByUniqueID(aUniqueID);
1105 if (child) return child;
1106
1107 uint32_t childDocCount = mChildDocuments.Length();
1108 for (uint32_t childDocIdx = 0; childDocIdx < childDocCount; childDocIdx++) {
1109 DocAccessible* childDocument = mChildDocuments.ElementAt(childDocIdx);
1110 child = childDocument->GetAccessibleByUniqueIDInSubtree(aUniqueID);
1111 if (child) return child;
1112 }
1113
1114 return nullptr;
1115 }
1116
GetAccessibleOrContainer(nsINode * aNode) const1117 Accessible* DocAccessible::GetAccessibleOrContainer(nsINode* aNode) const {
1118 if (!aNode || !aNode->GetComposedDoc()) return nullptr;
1119
1120 for (nsINode* currNode = aNode; currNode;
1121 currNode = currNode->GetFlattenedTreeParentNode()) {
1122 if (Accessible* accessible = GetAccessible(currNode)) {
1123 return accessible;
1124 }
1125 }
1126
1127 return nullptr;
1128 }
1129
GetAccessibleOrDescendant(nsINode * aNode) const1130 Accessible* DocAccessible::GetAccessibleOrDescendant(nsINode* aNode) const {
1131 Accessible* acc = GetAccessible(aNode);
1132 if (acc) return acc;
1133
1134 acc = GetContainerAccessible(aNode);
1135 if (acc) {
1136 uint32_t childCnt = acc->ChildCount();
1137 for (uint32_t idx = 0; idx < childCnt; idx++) {
1138 Accessible* child = acc->GetChildAt(idx);
1139 for (nsIContent* elm = child->GetContent();
1140 elm && elm != acc->GetContent();
1141 elm = elm->GetFlattenedTreeParent()) {
1142 if (elm == aNode) return child;
1143 }
1144 }
1145 }
1146
1147 return nullptr;
1148 }
1149
BindToDocument(Accessible * aAccessible,const nsRoleMapEntry * aRoleMapEntry)1150 void DocAccessible::BindToDocument(Accessible* aAccessible,
1151 const nsRoleMapEntry* aRoleMapEntry) {
1152 // Put into DOM node cache.
1153 if (aAccessible->IsNodeMapEntry())
1154 mNodeToAccessibleMap.Put(aAccessible->GetNode(), aAccessible);
1155
1156 // Put into unique ID cache.
1157 mAccessibleCache.Put(aAccessible->UniqueID(), aAccessible);
1158
1159 aAccessible->SetRoleMapEntry(aRoleMapEntry);
1160
1161 if (aAccessible->HasOwnContent()) {
1162 AddDependentIDsFor(aAccessible);
1163
1164 nsIContent* content = aAccessible->GetContent();
1165 if (content->IsElement() && content->AsElement()->HasAttr(
1166 kNameSpaceID_None, nsGkAtoms::aria_owns)) {
1167 mNotificationController->ScheduleRelocation(aAccessible);
1168 }
1169 }
1170 }
1171
UnbindFromDocument(Accessible * aAccessible)1172 void DocAccessible::UnbindFromDocument(Accessible* aAccessible) {
1173 NS_ASSERTION(mAccessibleCache.GetWeak(aAccessible->UniqueID()),
1174 "Unbinding the unbound accessible!");
1175
1176 // Fire focus event on accessible having DOM focus if active item was removed
1177 // from the tree.
1178 if (FocusMgr()->IsActiveItem(aAccessible)) {
1179 FocusMgr()->ActiveItemChanged(nullptr);
1180 #ifdef A11Y_LOG
1181 if (logging::IsEnabled(logging::eFocus))
1182 logging::ActiveItemChangeCausedBy("tree shutdown", aAccessible);
1183 #endif
1184 }
1185
1186 // Remove an accessible from node-to-accessible map if it exists there.
1187 if (aAccessible->IsNodeMapEntry() &&
1188 mNodeToAccessibleMap.Get(aAccessible->GetNode()) == aAccessible)
1189 mNodeToAccessibleMap.Remove(aAccessible->GetNode());
1190
1191 aAccessible->mStateFlags |= eIsNotInDocument;
1192
1193 // Update XPCOM part.
1194 xpcAccessibleDocument* xpcDoc = GetAccService()->GetCachedXPCDocument(this);
1195 if (xpcDoc) xpcDoc->NotifyOfShutdown(aAccessible);
1196
1197 void* uniqueID = aAccessible->UniqueID();
1198
1199 NS_ASSERTION(!aAccessible->IsDefunct(), "Shutdown the shutdown accessible!");
1200 aAccessible->Shutdown();
1201
1202 mAccessibleCache.Remove(uniqueID);
1203 }
1204
ContentInserted(nsIContent * aContainerNode,nsIContent * aStartChildNode,nsIContent * aEndChildNode)1205 void DocAccessible::ContentInserted(nsIContent* aContainerNode,
1206 nsIContent* aStartChildNode,
1207 nsIContent* aEndChildNode) {
1208 // Ignore content insertions until we constructed accessible tree. Otherwise
1209 // schedule tree update on content insertion after layout.
1210 if (mNotificationController && HasLoadState(eTreeConstructed)) {
1211 // Update the whole tree of this document accessible when the container is
1212 // null (document element is inserted or removed).
1213 Accessible* container =
1214 aContainerNode ? AccessibleOrTrueContainer(aContainerNode) : this;
1215 if (container) {
1216 // Ignore notification if the container node is no longer in the DOM tree.
1217 mNotificationController->ScheduleContentInsertion(
1218 container, aStartChildNode, aEndChildNode);
1219 }
1220 }
1221 }
1222
RecreateAccessible(nsIContent * aContent)1223 void DocAccessible::RecreateAccessible(nsIContent* aContent) {
1224 #ifdef A11Y_LOG
1225 if (logging::IsEnabled(logging::eTree)) {
1226 logging::MsgBegin("TREE", "accessible recreated");
1227 logging::Node("content", aContent);
1228 logging::MsgEnd();
1229 }
1230 #endif
1231
1232 // XXX: we shouldn't recreate whole accessible subtree, instead we should
1233 // subclass hide and show events to handle them separately and implement their
1234 // coalescence with normal hide and show events. Note, in this case they
1235 // should be coalesced with normal show/hide events.
1236
1237 nsIContent* parent = aContent->GetFlattenedTreeParent();
1238 ContentRemoved(aContent);
1239 ContentInserted(parent, aContent, aContent->GetNextSibling());
1240 }
1241
ProcessInvalidationList()1242 void DocAccessible::ProcessInvalidationList() {
1243 // Invalidate children of container accessible for each element in
1244 // invalidation list. Allow invalidation list insertions while container
1245 // children are recached.
1246 for (uint32_t idx = 0; idx < mInvalidationList.Length(); idx++) {
1247 nsIContent* content = mInvalidationList[idx];
1248 if (!HasAccessible(content) && content->HasID()) {
1249 Accessible* container = GetContainerAccessible(content);
1250 if (container) {
1251 // Check if the node is a target of aria-owns, and if so, don't process
1252 // it here and let DoARIAOwnsRelocation process it.
1253 AttrRelProviderArray* list =
1254 mDependentIDsHash.Get(nsDependentAtomString(content->GetID()));
1255 bool shouldProcess = !!list;
1256 if (shouldProcess) {
1257 for (uint32_t idx = 0; idx < list->Length(); idx++) {
1258 if (list->ElementAt(idx)->mRelAttr == nsGkAtoms::aria_owns) {
1259 shouldProcess = false;
1260 break;
1261 }
1262 }
1263
1264 if (shouldProcess) {
1265 ProcessContentInserted(container, content);
1266 }
1267 }
1268 }
1269 }
1270 }
1271
1272 mInvalidationList.Clear();
1273 }
1274
GetAccessibleEvenIfNotInMap(nsINode * aNode) const1275 Accessible* DocAccessible::GetAccessibleEvenIfNotInMap(nsINode* aNode) const {
1276 if (!aNode->IsContent() ||
1277 !aNode->AsContent()->IsHTMLElement(nsGkAtoms::area))
1278 return GetAccessible(aNode);
1279
1280 // XXX Bug 135040, incorrect when multiple images use the same map.
1281 nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame();
1282 nsImageFrame* imageFrame = do_QueryFrame(frame);
1283 if (imageFrame) {
1284 Accessible* parent = GetAccessible(imageFrame->GetContent());
1285 if (parent) {
1286 Accessible* area = parent->AsImageMap()->GetChildAccessibleFor(aNode);
1287 if (area) return area;
1288
1289 return nullptr;
1290 }
1291 }
1292
1293 return GetAccessible(aNode);
1294 }
1295
1296 ////////////////////////////////////////////////////////////////////////////////
1297 // Protected members
1298
NotifyOfLoading(bool aIsReloading)1299 void DocAccessible::NotifyOfLoading(bool aIsReloading) {
1300 // Mark the document accessible as loading, if it stays alive then we'll mark
1301 // it as loaded when we receive proper notification.
1302 mLoadState &= ~eDOMLoaded;
1303
1304 if (!IsLoadEventTarget()) return;
1305
1306 if (aIsReloading && !mLoadEventType) {
1307 // Fire reload and state busy events on existing document accessible while
1308 // event from user input flag can be calculated properly and accessible
1309 // is alive. When new document gets loaded then this one is destroyed.
1310 RefPtr<AccEvent> reloadEvent =
1311 new AccEvent(nsIAccessibleEvent::EVENT_DOCUMENT_RELOAD, this);
1312 nsEventShell::FireEvent(reloadEvent);
1313 }
1314
1315 // Fire state busy change event. Use delayed event since we don't care
1316 // actually if event isn't delivered when the document goes away like a shot.
1317 RefPtr<AccEvent> stateEvent =
1318 new AccStateChangeEvent(this, states::BUSY, true);
1319 FireDelayedEvent(stateEvent);
1320 }
1321
DoInitialUpdate()1322 void DocAccessible::DoInitialUpdate() {
1323 if (nsCoreUtils::IsTabDocument(mDocumentNode)) {
1324 mDocFlags |= eTabDocument;
1325 if (IPCAccessibilityActive()) {
1326 nsIDocShell* docShell = mDocumentNode->GetDocShell();
1327 if (RefPtr<dom::TabChild> tabChild = dom::TabChild::GetFrom(docShell)) {
1328 DocAccessibleChild* ipcDoc = new DocAccessibleChild(this, tabChild);
1329 SetIPCDoc(ipcDoc);
1330 if (IsRoot()) {
1331 tabChild->SetTopLevelDocAccessibleChild(ipcDoc);
1332 }
1333
1334 #if defined(XP_WIN)
1335 IAccessibleHolder holder(CreateHolderFromAccessible(WrapNotNull(this)));
1336 MOZ_DIAGNOSTIC_ASSERT(!holder.IsNull());
1337 int32_t childID = AccessibleWrap::GetChildIDFor(this);
1338 #else
1339 int32_t holder = 0, childID = 0;
1340 #endif
1341 tabChild->SendPDocAccessibleConstructor(ipcDoc, nullptr, 0, childID,
1342 holder);
1343 }
1344 }
1345 }
1346
1347 mLoadState |= eTreeConstructed;
1348
1349 // Set up a root element and ARIA role mapping.
1350 UpdateRootElIfNeeded();
1351
1352 // Build initial tree.
1353 CacheChildrenInSubtree(this);
1354 #ifdef A11Y_LOG
1355 if (logging::IsEnabled(logging::eVerbose)) {
1356 logging::Tree("TREE", "Initial subtree", this);
1357 }
1358 #endif
1359
1360 // Fire reorder event after the document tree is constructed. Note, since
1361 // this reorder event is processed by parent document then events targeted to
1362 // this document may be fired prior to this reorder event. If this is
1363 // a problem then consider to keep event processing per tab document.
1364 if (!IsRoot()) {
1365 RefPtr<AccReorderEvent> reorderEvent = new AccReorderEvent(Parent());
1366 ParentDocument()->FireDelayedEvent(reorderEvent);
1367 }
1368
1369 if (IPCAccessibilityActive()) {
1370 DocAccessibleChild* ipcDoc = IPCDoc();
1371 MOZ_ASSERT(ipcDoc);
1372 if (ipcDoc) {
1373 for (auto idx = 0U; idx < mChildren.Length(); idx++) {
1374 ipcDoc->InsertIntoIpcTree(this, mChildren.ElementAt(idx), idx);
1375 }
1376 }
1377 }
1378 }
1379
ProcessLoad()1380 void DocAccessible::ProcessLoad() {
1381 mLoadState |= eCompletelyLoaded;
1382
1383 #ifdef A11Y_LOG
1384 if (logging::IsEnabled(logging::eDocLoad))
1385 logging::DocCompleteLoad(this, IsLoadEventTarget());
1386 #endif
1387
1388 // Do not fire document complete/stop events for root chrome document
1389 // accessibles and for frame/iframe documents because
1390 // a) screen readers start working on focus event in the case of root chrome
1391 // documents
1392 // b) document load event on sub documents causes screen readers to act is if
1393 // entire page is reloaded.
1394 if (!IsLoadEventTarget()) return;
1395
1396 // Fire complete/load stopped if the load event type is given.
1397 if (mLoadEventType) {
1398 RefPtr<AccEvent> loadEvent = new AccEvent(mLoadEventType, this);
1399 FireDelayedEvent(loadEvent);
1400
1401 mLoadEventType = 0;
1402 }
1403
1404 // Fire busy state change event.
1405 RefPtr<AccEvent> stateEvent =
1406 new AccStateChangeEvent(this, states::BUSY, false);
1407 FireDelayedEvent(stateEvent);
1408 }
1409
AddDependentIDsFor(Accessible * aRelProvider,nsAtom * aRelAttr)1410 void DocAccessible::AddDependentIDsFor(Accessible* aRelProvider,
1411 nsAtom* aRelAttr) {
1412 dom::Element* relProviderEl = aRelProvider->Elm();
1413 if (!relProviderEl) return;
1414
1415 for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) {
1416 nsAtom* relAttr = *kRelationAttrs[idx];
1417 if (aRelAttr && aRelAttr != relAttr) continue;
1418
1419 if (relAttr == nsGkAtoms::_for) {
1420 if (!relProviderEl->IsAnyOfHTMLElements(nsGkAtoms::label,
1421 nsGkAtoms::output))
1422 continue;
1423
1424 } else if (relAttr == nsGkAtoms::control) {
1425 if (!relProviderEl->IsAnyOfXULElements(nsGkAtoms::label,
1426 nsGkAtoms::description))
1427 continue;
1428 }
1429
1430 IDRefsIterator iter(this, relProviderEl, relAttr);
1431 while (true) {
1432 const nsDependentSubstring id = iter.NextID();
1433 if (id.IsEmpty()) break;
1434
1435 nsIContent* dependentContent = iter.GetElem(id);
1436 if (relAttr == nsGkAtoms::aria_owns && dependentContent &&
1437 !aRelProvider->IsAcceptableChild(dependentContent))
1438 continue;
1439
1440 AttrRelProviderArray* providers = mDependentIDsHash.Get(id);
1441 if (!providers) {
1442 providers = new AttrRelProviderArray();
1443 if (providers) {
1444 mDependentIDsHash.Put(id, providers);
1445 }
1446 }
1447
1448 if (providers) {
1449 AttrRelProvider* provider = new AttrRelProvider(relAttr, relProviderEl);
1450 if (provider) {
1451 providers->AppendElement(provider);
1452
1453 // We've got here during the children caching. If the referenced
1454 // content is not accessible then store it to pend its container
1455 // children invalidation (this happens immediately after the caching
1456 // is finished).
1457 if (dependentContent) {
1458 if (!HasAccessible(dependentContent)) {
1459 mInvalidationList.AppendElement(dependentContent);
1460 }
1461 }
1462 }
1463 }
1464 }
1465
1466 // If the relation attribute is given then we don't have anything else to
1467 // check.
1468 if (aRelAttr) break;
1469 }
1470
1471 // Make sure to schedule the tree update if needed.
1472 mNotificationController->ScheduleProcessing();
1473 }
1474
RemoveDependentIDsFor(Accessible * aRelProvider,nsAtom * aRelAttr)1475 void DocAccessible::RemoveDependentIDsFor(Accessible* aRelProvider,
1476 nsAtom* aRelAttr) {
1477 dom::Element* relProviderElm = aRelProvider->Elm();
1478 if (!relProviderElm) return;
1479
1480 for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) {
1481 nsAtom* relAttr = *kRelationAttrs[idx];
1482 if (aRelAttr && aRelAttr != *kRelationAttrs[idx]) continue;
1483
1484 IDRefsIterator iter(this, relProviderElm, relAttr);
1485 while (true) {
1486 const nsDependentSubstring id = iter.NextID();
1487 if (id.IsEmpty()) break;
1488
1489 AttrRelProviderArray* providers = mDependentIDsHash.Get(id);
1490 if (providers) {
1491 for (uint32_t jdx = 0; jdx < providers->Length();) {
1492 AttrRelProvider* provider = (*providers)[jdx];
1493 if (provider->mRelAttr == relAttr &&
1494 provider->mContent == relProviderElm)
1495 providers->RemoveElement(provider);
1496 else
1497 jdx++;
1498 }
1499 if (providers->Length() == 0) mDependentIDsHash.Remove(id);
1500 }
1501 }
1502
1503 // If the relation attribute is given then we don't have anything else to
1504 // check.
1505 if (aRelAttr) break;
1506 }
1507 }
1508
UpdateAccessibleOnAttrChange(dom::Element * aElement,nsAtom * aAttribute)1509 bool DocAccessible::UpdateAccessibleOnAttrChange(dom::Element* aElement,
1510 nsAtom* aAttribute) {
1511 if (aAttribute == nsGkAtoms::role) {
1512 // It is common for js libraries to set the role on the body element after
1513 // the document has loaded. In this case we just update the role map entry.
1514 if (mContent == aElement) {
1515 SetRoleMapEntry(aria::GetRoleMap(aElement));
1516 if (mIPCDoc) {
1517 mIPCDoc->SendRoleChangedEvent(Role());
1518 }
1519
1520 return true;
1521 }
1522
1523 // Recreate the accessible when role is changed because we might require a
1524 // different accessible class for the new role or the accessible may expose
1525 // a different sets of interfaces (COM restriction).
1526 RecreateAccessible(aElement);
1527
1528 return true;
1529 }
1530
1531 if (aAttribute == nsGkAtoms::href) {
1532 // Not worth the expense to ensure which namespace these are in. It doesn't
1533 // kill use to recreate the accessible even if the attribute was used in
1534 // the wrong namespace or an element that doesn't support it.
1535
1536 // Make sure the accessible is recreated asynchronously to allow the content
1537 // to handle the attribute change.
1538 RecreateAccessible(aElement);
1539 return true;
1540 }
1541
1542 if (aAttribute == nsGkAtoms::aria_multiselectable &&
1543 aElement->HasAttr(kNameSpaceID_None, nsGkAtoms::role)) {
1544 // This affects whether the accessible supports SelectAccessible.
1545 // COM says we cannot change what interfaces are supported on-the-fly,
1546 // so invalidate this object. A new one will be created on demand.
1547 RecreateAccessible(aElement);
1548
1549 return true;
1550 }
1551
1552 return false;
1553 }
1554
UpdateRootElIfNeeded()1555 void DocAccessible::UpdateRootElIfNeeded() {
1556 dom::Element* rootEl = mDocumentNode->GetBodyElement();
1557 if (!rootEl) {
1558 rootEl = mDocumentNode->GetRootElement();
1559 }
1560 if (rootEl != mContent) {
1561 mContent = rootEl;
1562 SetRoleMapEntry(aria::GetRoleMap(rootEl));
1563 if (mIPCDoc) {
1564 mIPCDoc->SendRoleChangedEvent(Role());
1565 }
1566 }
1567 }
1568
1569 /**
1570 * Content insertion helper.
1571 */
1572 class InsertIterator final {
1573 public:
InsertIterator(Accessible * aContext,const nsTArray<nsCOMPtr<nsIContent>> * aNodes)1574 InsertIterator(Accessible* aContext,
1575 const nsTArray<nsCOMPtr<nsIContent> >* aNodes)
1576 : mChild(nullptr),
1577 mChildBefore(nullptr),
1578 mWalker(aContext),
1579 mNodes(aNodes),
1580 mNodesIdx(0) {
1581 MOZ_ASSERT(aContext, "No context");
1582 MOZ_ASSERT(aNodes, "No nodes to search for accessible elements");
1583 MOZ_COUNT_CTOR(InsertIterator);
1584 }
~InsertIterator()1585 ~InsertIterator() { MOZ_COUNT_DTOR(InsertIterator); }
1586
Context() const1587 Accessible* Context() const { return mWalker.Context(); }
Child() const1588 Accessible* Child() const { return mChild; }
ChildBefore() const1589 Accessible* ChildBefore() const { return mChildBefore; }
Document() const1590 DocAccessible* Document() const { return mWalker.Document(); }
1591
1592 /**
1593 * Iterates to a next accessible within the inserted content.
1594 */
1595 bool Next();
1596
Rejected()1597 void Rejected() {
1598 mChild = nullptr;
1599 mChildBefore = nullptr;
1600 }
1601
1602 private:
1603 Accessible* mChild;
1604 Accessible* mChildBefore;
1605 TreeWalker mWalker;
1606
1607 const nsTArray<nsCOMPtr<nsIContent> >* mNodes;
1608 uint32_t mNodesIdx;
1609 };
1610
Next()1611 bool InsertIterator::Next() {
1612 if (mNodesIdx > 0) {
1613 Accessible* nextChild = mWalker.Next();
1614 if (nextChild) {
1615 mChildBefore = mChild;
1616 mChild = nextChild;
1617 return true;
1618 }
1619 }
1620
1621 while (mNodesIdx < mNodes->Length()) {
1622 // Ignore nodes that are not contained by the container anymore.
1623
1624 // The container might be changed, for example, because of the subsequent
1625 // overlapping content insertion (i.e. other content was inserted between
1626 // this inserted content and its container or the content was reinserted
1627 // into different container of unrelated part of tree). To avoid a double
1628 // processing of the content insertion ignore this insertion notification.
1629 // Note, the inserted content might be not in tree at all at this point
1630 // what means there's no container. Ignore the insertion too.
1631 nsIContent* prevNode = mNodes->SafeElementAt(mNodesIdx - 1);
1632 nsIContent* node = mNodes->ElementAt(mNodesIdx++);
1633 Accessible* container = Document()->AccessibleOrTrueContainer(node);
1634 if (container != Context()) {
1635 continue;
1636 }
1637
1638 // HTML comboboxes have no-content list accessible as an intermediate
1639 // containing all options.
1640 if (container->IsHTMLCombobox()) {
1641 container = container->FirstChild();
1642 }
1643
1644 if (!container->IsAcceptableChild(node)) {
1645 continue;
1646 }
1647
1648 #ifdef A11Y_LOG
1649 logging::TreeInfo("traversing an inserted node", logging::eVerbose,
1650 "container", container, "node", node);
1651 #endif
1652
1653 // If inserted nodes are siblings then just move the walker next.
1654 if (mChild && prevNode && prevNode->GetNextSibling() == node) {
1655 Accessible* nextChild = mWalker.Scope(node);
1656 if (nextChild) {
1657 mChildBefore = mChild;
1658 mChild = nextChild;
1659 return true;
1660 }
1661 } else {
1662 TreeWalker finder(container);
1663 if (finder.Seek(node)) {
1664 mChild = mWalker.Scope(node);
1665 if (mChild) {
1666 MOZ_ASSERT(!mChild->IsRelocated(), "child cannot be aria owned");
1667 mChildBefore = finder.Prev();
1668 return true;
1669 }
1670 }
1671 }
1672 }
1673
1674 return false;
1675 }
1676
ProcessContentInserted(Accessible * aContainer,const nsTArray<nsCOMPtr<nsIContent>> * aNodes)1677 void DocAccessible::ProcessContentInserted(
1678 Accessible* aContainer, const nsTArray<nsCOMPtr<nsIContent> >* aNodes) {
1679 // Process insertions if the container accessible is still in tree.
1680 if (!aContainer->IsInDocument()) {
1681 return;
1682 }
1683
1684 // If new root content has been inserted then update it.
1685 if (aContainer == this) {
1686 UpdateRootElIfNeeded();
1687 }
1688
1689 InsertIterator iter(aContainer, aNodes);
1690 if (!iter.Next()) {
1691 return;
1692 }
1693
1694 #ifdef A11Y_LOG
1695 logging::TreeInfo("children before insertion", logging::eVerbose, aContainer);
1696 #endif
1697
1698 TreeMutation mt(aContainer);
1699 do {
1700 Accessible* parent = iter.Child()->Parent();
1701 if (parent) {
1702 if (parent != aContainer) {
1703 #ifdef A11Y_LOG
1704 logging::TreeInfo("stealing accessible", 0, "old parent", parent,
1705 "new parent", aContainer, "child", iter.Child(),
1706 nullptr);
1707 #endif
1708 MOZ_ASSERT_UNREACHABLE("stealing accessible");
1709 continue;
1710 }
1711
1712 #ifdef A11Y_LOG
1713 logging::TreeInfo("binding to same parent", logging::eVerbose, "parent",
1714 aContainer, "child", iter.Child(), nullptr);
1715 #endif
1716 continue;
1717 }
1718
1719 if (aContainer->InsertAfter(iter.Child(), iter.ChildBefore())) {
1720 #ifdef A11Y_LOG
1721 logging::TreeInfo("accessible was inserted", 0, "container", aContainer,
1722 "child", iter.Child(), nullptr);
1723 #endif
1724
1725 CreateSubtree(iter.Child());
1726 mt.AfterInsertion(iter.Child());
1727 continue;
1728 }
1729
1730 MOZ_ASSERT_UNREACHABLE("accessible was rejected");
1731 iter.Rejected();
1732 } while (iter.Next());
1733
1734 mt.Done();
1735
1736 #ifdef A11Y_LOG
1737 logging::TreeInfo("children after insertion", logging::eVerbose, aContainer);
1738 #endif
1739
1740 FireEventsOnInsertion(aContainer);
1741 }
1742
ProcessContentInserted(Accessible * aContainer,nsIContent * aNode)1743 void DocAccessible::ProcessContentInserted(Accessible* aContainer,
1744 nsIContent* aNode) {
1745 if (!aContainer->IsInDocument()) {
1746 return;
1747 }
1748
1749 #ifdef A11Y_LOG
1750 logging::TreeInfo("children before insertion", logging::eVerbose, aContainer);
1751 #endif
1752
1753 #ifdef A11Y_LOG
1754 logging::TreeInfo("traversing an inserted node", logging::eVerbose,
1755 "container", aContainer, "node", aNode);
1756 #endif
1757
1758 TreeWalker walker(aContainer);
1759 if (aContainer->IsAcceptableChild(aNode) && walker.Seek(aNode)) {
1760 Accessible* child = GetAccessible(aNode);
1761 if (!child) {
1762 child = GetAccService()->CreateAccessible(aNode, aContainer);
1763 }
1764
1765 if (child) {
1766 TreeMutation mt(aContainer);
1767 if (!aContainer->InsertAfter(child, walker.Prev())) {
1768 return;
1769 }
1770 CreateSubtree(child);
1771 mt.AfterInsertion(child);
1772 mt.Done();
1773
1774 FireEventsOnInsertion(aContainer);
1775 }
1776 }
1777
1778 #ifdef A11Y_LOG
1779 logging::TreeInfo("children after insertion", logging::eVerbose, aContainer);
1780 #endif
1781 }
1782
FireEventsOnInsertion(Accessible * aContainer)1783 void DocAccessible::FireEventsOnInsertion(Accessible* aContainer) {
1784 // Check to see if change occurred inside an alert, and fire an EVENT_ALERT
1785 // if it did.
1786 if (aContainer->IsAlert() || aContainer->IsInsideAlert()) {
1787 Accessible* ancestor = aContainer;
1788 do {
1789 if (ancestor->IsAlert()) {
1790 FireDelayedEvent(nsIAccessibleEvent::EVENT_ALERT, ancestor);
1791 break;
1792 }
1793 } while ((ancestor = ancestor->Parent()));
1794 }
1795 }
1796
ContentRemoved(Accessible * aChild)1797 void DocAccessible::ContentRemoved(Accessible* aChild) {
1798 Accessible* parent = aChild->Parent();
1799 MOZ_DIAGNOSTIC_ASSERT(parent, "Unattached accessible from tree");
1800
1801 #ifdef A11Y_LOG
1802 logging::TreeInfo("process content removal", 0, "container", parent, "child",
1803 aChild, nullptr);
1804 #endif
1805
1806 // XXX: event coalescence may kill us
1807 RefPtr<Accessible> kungFuDeathGripChild(aChild);
1808
1809 TreeMutation mt(parent);
1810 mt.BeforeRemoval(aChild);
1811
1812 if (aChild->IsDefunct()) {
1813 MOZ_ASSERT_UNREACHABLE("Event coalescence killed the accessible");
1814 mt.Done();
1815 return;
1816 }
1817
1818 MOZ_DIAGNOSTIC_ASSERT(aChild->Parent(), "Alive but unparented #1");
1819
1820 if (aChild->IsRelocated()) {
1821 nsTArray<RefPtr<Accessible> >* owned = mARIAOwnsHash.Get(parent);
1822 MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash");
1823 owned->RemoveElement(aChild);
1824 if (owned->Length() == 0) {
1825 mARIAOwnsHash.Remove(parent);
1826 }
1827 }
1828 MOZ_DIAGNOSTIC_ASSERT(aChild->Parent(), "Unparented #2");
1829 parent->RemoveChild(aChild);
1830 UncacheChildrenInSubtree(aChild);
1831
1832 mt.Done();
1833 }
1834
ContentRemoved(nsIContent * aContentNode)1835 void DocAccessible::ContentRemoved(nsIContent* aContentNode) {
1836 // If child node is not accessible then look for its accessible children.
1837 Accessible* acc = GetAccessible(aContentNode);
1838 if (acc) {
1839 ContentRemoved(acc);
1840 }
1841
1842 dom::AllChildrenIterator iter =
1843 dom::AllChildrenIterator(aContentNode, nsIContent::eAllChildren, true);
1844 while (nsIContent* childNode = iter.GetNextChild()) {
1845 ContentRemoved(childNode);
1846 }
1847 }
1848
RelocateARIAOwnedIfNeeded(nsIContent * aElement)1849 bool DocAccessible::RelocateARIAOwnedIfNeeded(nsIContent* aElement) {
1850 if (!aElement->HasID()) return false;
1851
1852 AttrRelProviderArray* list =
1853 mDependentIDsHash.Get(nsDependentAtomString(aElement->GetID()));
1854 if (list) {
1855 for (uint32_t idx = 0; idx < list->Length(); idx++) {
1856 if (list->ElementAt(idx)->mRelAttr == nsGkAtoms::aria_owns) {
1857 Accessible* owner = GetAccessible(list->ElementAt(idx)->mContent);
1858 if (owner) {
1859 mNotificationController->ScheduleRelocation(owner);
1860 return true;
1861 }
1862 }
1863 }
1864 }
1865
1866 return false;
1867 }
1868
DoARIAOwnsRelocation(Accessible * aOwner)1869 void DocAccessible::DoARIAOwnsRelocation(Accessible* aOwner) {
1870 MOZ_ASSERT(aOwner, "aOwner must be a valid pointer");
1871 MOZ_ASSERT(aOwner->Elm(), "aOwner->Elm() must be a valid pointer");
1872
1873 #ifdef A11Y_LOG
1874 logging::TreeInfo("aria owns relocation", logging::eVerbose, aOwner);
1875 #endif
1876
1877 nsTArray<RefPtr<Accessible> >* owned = mARIAOwnsHash.LookupOrAdd(aOwner);
1878
1879 IDRefsIterator iter(this, aOwner->Elm(), nsGkAtoms::aria_owns);
1880 uint32_t idx = 0;
1881 while (nsIContent* childEl = iter.NextElem()) {
1882 Accessible* child = GetAccessible(childEl);
1883 auto insertIdx = aOwner->ChildCount() - owned->Length() + idx;
1884
1885 // Make an attempt to create an accessible if it wasn't created yet.
1886 if (!child) {
1887 if (aOwner->IsAcceptableChild(childEl)) {
1888 child = GetAccService()->CreateAccessible(childEl, aOwner);
1889 if (child) {
1890 TreeMutation imut(aOwner);
1891 aOwner->InsertChildAt(insertIdx, child);
1892 imut.AfterInsertion(child);
1893 imut.Done();
1894
1895 child->SetRelocated(true);
1896 owned->InsertElementAt(idx, child);
1897 idx++;
1898
1899 // Create subtree before adjusting the insertion index, since subtree
1900 // creation may alter children in the container.
1901 CreateSubtree(child);
1902 FireEventsOnInsertion(aOwner);
1903 }
1904 }
1905 continue;
1906 }
1907
1908 #ifdef A11Y_LOG
1909 logging::TreeInfo("aria owns traversal", logging::eVerbose, "candidate",
1910 child, nullptr);
1911 #endif
1912
1913 if (owned->IndexOf(child) < idx) {
1914 continue; // ignore second entry of same ID
1915 }
1916
1917 // Same child on same position, no change.
1918 if (child->Parent() == aOwner) {
1919 int32_t indexInParent = child->IndexInParent();
1920
1921 // The child is being placed in its current index,
1922 // eg. aria-owns='id1 id2 id3' is changed to aria-owns='id3 id2 id1'.
1923 if (indexInParent == static_cast<int32_t>(insertIdx)) {
1924 MOZ_ASSERT(child->IsRelocated(),
1925 "A child, having an index in parent from aria ownded "
1926 "indices range, has to be aria owned");
1927 MOZ_ASSERT(owned->ElementAt(idx) == child,
1928 "Unexpected child in ARIA owned array");
1929 idx++;
1930 continue;
1931 }
1932
1933 // The child is being inserted directly after its current index,
1934 // resulting in a no-move case. This will happen when a parent aria-owns
1935 // its last ordinal child:
1936 // <ul aria-owns='id2'><li id='id1'></li><li id='id2'></li></ul>
1937 if (indexInParent == static_cast<int32_t>(insertIdx) - 1) {
1938 MOZ_ASSERT(!child->IsRelocated(),
1939 "Child should be in its ordinal position");
1940 child->SetRelocated(true);
1941 owned->InsertElementAt(idx, child);
1942 idx++;
1943 continue;
1944 }
1945 }
1946
1947 MOZ_ASSERT(owned->SafeElementAt(idx) != child, "Already in place!");
1948
1949 // A new child is found, check for loops.
1950 if (child->Parent() != aOwner) {
1951 // Child is aria-owned by another container, skip.
1952 if (child->IsRelocated()) {
1953 continue;
1954 }
1955
1956 Accessible* parent = aOwner;
1957 while (parent && parent != child && !parent->IsDoc()) {
1958 parent = parent->Parent();
1959 }
1960 // A referred child cannot be a parent of the owner.
1961 if (parent == child) {
1962 continue;
1963 }
1964 }
1965
1966 if (MoveChild(child, aOwner, insertIdx)) {
1967 child->SetRelocated(true);
1968 MOZ_ASSERT(owned == mARIAOwnsHash.Get(aOwner));
1969 owned = mARIAOwnsHash.LookupOrAdd(aOwner);
1970 owned->InsertElementAt(idx, child);
1971 idx++;
1972 }
1973 }
1974
1975 // Put back children that are not seized anymore.
1976 PutChildrenBack(owned, idx);
1977 if (owned->Length() == 0) {
1978 mARIAOwnsHash.Remove(aOwner);
1979 }
1980 }
1981
PutChildrenBack(nsTArray<RefPtr<Accessible>> * aChildren,uint32_t aStartIdx)1982 void DocAccessible::PutChildrenBack(nsTArray<RefPtr<Accessible> >* aChildren,
1983 uint32_t aStartIdx) {
1984 MOZ_ASSERT(aStartIdx <= aChildren->Length(), "Wrong removal index");
1985
1986 for (auto idx = aStartIdx; idx < aChildren->Length(); idx++) {
1987 Accessible* child = aChildren->ElementAt(idx);
1988 if (!child->IsInDocument()) {
1989 continue;
1990 }
1991
1992 // Remove the child from the owner
1993 Accessible* owner = child->Parent();
1994 if (!owner) {
1995 NS_ERROR("Cannot put the child back. No parent, a broken tree.");
1996 continue;
1997 }
1998
1999 #ifdef A11Y_LOG
2000 logging::TreeInfo("aria owns put child back", 0, "old parent", owner,
2001 "child", child, nullptr);
2002 #endif
2003
2004 // Unset relocated flag to find an insertion point for the child.
2005 child->SetRelocated(false);
2006
2007 nsIContent* content = child->GetContent();
2008 int32_t idxInParent = -1;
2009 Accessible* origContainer =
2010 AccessibleOrTrueContainer(content->GetFlattenedTreeParentNode());
2011 if (origContainer) {
2012 TreeWalker walker(origContainer);
2013 if (walker.Seek(content)) {
2014 Accessible* prevChild = walker.Prev();
2015 if (prevChild) {
2016 idxInParent = prevChild->IndexInParent() + 1;
2017 MOZ_DIAGNOSTIC_ASSERT(origContainer == prevChild->Parent(),
2018 "Broken tree");
2019 origContainer = prevChild->Parent();
2020 } else {
2021 idxInParent = 0;
2022 }
2023 }
2024 }
2025
2026 // The child may have already be in its ordinal place for 2 reasons:
2027 // 1. It was the last ordinal child, and the first aria-owned child.
2028 // given: <ul id="list" aria-owns="b"><li id="a"></li><li
2029 // id="b"></li></ul> after load: $("list").setAttribute("aria-owns", "");
2030 // 2. The preceding adopted children were just reclaimed, eg:
2031 // given: <ul id="list"><li id="b"></li></ul>
2032 // after load: $("list").setAttribute("aria-owns", "a b");
2033 // later: $("list").setAttribute("aria-owns", "");
2034 if (origContainer != owner || child->IndexInParent() != idxInParent) {
2035 DebugOnly<bool> moved = MoveChild(child, origContainer, idxInParent);
2036 MOZ_ASSERT(moved, "Failed to put child back.");
2037 } else {
2038 MOZ_ASSERT(!child->PrevSibling() || !child->PrevSibling()->IsRelocated(),
2039 "No relocated child should appear before this one");
2040 MOZ_ASSERT(!child->NextSibling() || child->NextSibling()->IsRelocated(),
2041 "No ordinal child should appear after this one");
2042 }
2043 }
2044
2045 aChildren->RemoveElementsAt(aStartIdx, aChildren->Length() - aStartIdx);
2046 }
2047
MoveChild(Accessible * aChild,Accessible * aNewParent,int32_t aIdxInParent)2048 bool DocAccessible::MoveChild(Accessible* aChild, Accessible* aNewParent,
2049 int32_t aIdxInParent) {
2050 MOZ_ASSERT(aChild, "No child");
2051 MOZ_ASSERT(aChild->Parent(), "No parent");
2052 MOZ_ASSERT(aIdxInParent <= static_cast<int32_t>(aNewParent->ChildCount()),
2053 "Wrong insertion point for a moving child");
2054
2055 Accessible* curParent = aChild->Parent();
2056
2057 if (!aNewParent->IsAcceptableChild(aChild->GetContent())) {
2058 return false;
2059 }
2060
2061 #ifdef A11Y_LOG
2062 logging::TreeInfo("move child", 0, "old parent", curParent, "new parent",
2063 aNewParent, "child", aChild, nullptr);
2064 #endif
2065
2066 // Forget aria-owns info in case of ARIA owned element. The caller is expected
2067 // to update it if needed.
2068 if (aChild->IsRelocated()) {
2069 aChild->SetRelocated(false);
2070 nsTArray<RefPtr<Accessible> >* owned = mARIAOwnsHash.Get(curParent);
2071 MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash");
2072 owned->RemoveElement(aChild);
2073 if (owned->Length() == 0) {
2074 mARIAOwnsHash.Remove(curParent);
2075 }
2076 }
2077
2078 NotificationController::MoveGuard mguard(mNotificationController);
2079
2080 if (curParent == aNewParent) {
2081 MOZ_ASSERT(aChild->IndexInParent() != aIdxInParent, "No move case");
2082 curParent->MoveChild(aIdxInParent, aChild);
2083
2084 #ifdef A11Y_LOG
2085 logging::TreeInfo("move child: parent tree after", logging::eVerbose,
2086 curParent);
2087 #endif
2088 return true;
2089 }
2090
2091 MOZ_ASSERT(aIdxInParent <= static_cast<int32_t>(aNewParent->ChildCount()),
2092 "Wrong insertion point for a moving child");
2093
2094 // If the child cannot be re-inserted into the tree, then make sure to remove
2095 // it from its present parent and then shutdown it.
2096 bool hasInsertionPoint =
2097 (aIdxInParent != -1) ||
2098 (aIdxInParent <= static_cast<int32_t>(aNewParent->ChildCount()));
2099
2100 TreeMutation rmut(curParent);
2101 rmut.BeforeRemoval(aChild, hasInsertionPoint && TreeMutation::kNoShutdown);
2102 curParent->RemoveChild(aChild);
2103 rmut.Done();
2104
2105 // No insertion point for the child.
2106 if (!hasInsertionPoint) {
2107 return true;
2108 }
2109
2110 TreeMutation imut(aNewParent);
2111 aNewParent->InsertChildAt(aIdxInParent, aChild);
2112 imut.AfterInsertion(aChild);
2113 imut.Done();
2114
2115 #ifdef A11Y_LOG
2116 logging::TreeInfo("move child: old parent tree after", logging::eVerbose,
2117 curParent);
2118 logging::TreeInfo("move child: new parent tree after", logging::eVerbose,
2119 aNewParent);
2120 #endif
2121
2122 return true;
2123 }
2124
CacheChildrenInSubtree(Accessible * aRoot,Accessible ** aFocusedAcc)2125 void DocAccessible::CacheChildrenInSubtree(Accessible* aRoot,
2126 Accessible** aFocusedAcc) {
2127 // If the accessible is focused then report a focus event after all related
2128 // mutation events.
2129 if (aFocusedAcc && !*aFocusedAcc &&
2130 FocusMgr()->HasDOMFocus(aRoot->GetContent()))
2131 *aFocusedAcc = aRoot;
2132
2133 Accessible* root = aRoot->IsHTMLCombobox() ? aRoot->FirstChild() : aRoot;
2134 if (root->KidsFromDOM()) {
2135 TreeMutation mt(root, TreeMutation::kNoEvents);
2136 TreeWalker walker(root);
2137 while (Accessible* child = walker.Next()) {
2138 if (child->IsBoundToParent()) {
2139 MoveChild(child, root, root->ChildCount());
2140 continue;
2141 }
2142
2143 root->AppendChild(child);
2144 mt.AfterInsertion(child);
2145
2146 CacheChildrenInSubtree(child, aFocusedAcc);
2147 }
2148 mt.Done();
2149 }
2150
2151 // Fire events for ARIA elements.
2152 if (!aRoot->HasARIARole()) {
2153 return;
2154 }
2155
2156 // XXX: we should delay document load complete event if the ARIA document
2157 // has aria-busy.
2158 roles::Role role = aRoot->ARIARole();
2159 if (!aRoot->IsDoc() &&
2160 (role == roles::DIALOG || role == roles::NON_NATIVE_DOCUMENT)) {
2161 FireDelayedEvent(nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE, aRoot);
2162 }
2163 }
2164
UncacheChildrenInSubtree(Accessible * aRoot)2165 void DocAccessible::UncacheChildrenInSubtree(Accessible* aRoot) {
2166 aRoot->mStateFlags |= eIsNotInDocument;
2167 RemoveDependentIDsFor(aRoot);
2168
2169 nsTArray<RefPtr<Accessible> >* owned = mARIAOwnsHash.Get(aRoot);
2170 uint32_t count = aRoot->ContentChildCount();
2171 for (uint32_t idx = 0; idx < count; idx++) {
2172 Accessible* child = aRoot->ContentChildAt(idx);
2173
2174 if (child->IsRelocated()) {
2175 MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash");
2176 owned->RemoveElement(child);
2177 if (owned->Length() == 0) {
2178 mARIAOwnsHash.Remove(aRoot);
2179 owned = nullptr;
2180 }
2181 }
2182
2183 // Removing this accessible from the document doesn't mean anything about
2184 // accessibles for subdocuments, so skip removing those from the tree.
2185 if (!child->IsDoc()) {
2186 UncacheChildrenInSubtree(child);
2187 }
2188 }
2189
2190 if (aRoot->IsNodeMapEntry() &&
2191 mNodeToAccessibleMap.Get(aRoot->GetNode()) == aRoot)
2192 mNodeToAccessibleMap.Remove(aRoot->GetNode());
2193 }
2194
ShutdownChildrenInSubtree(Accessible * aAccessible)2195 void DocAccessible::ShutdownChildrenInSubtree(Accessible* aAccessible) {
2196 // Traverse through children and shutdown them before this accessible. When
2197 // child gets shutdown then it removes itself from children array of its
2198 // parent. Use jdx index to process the cases if child is not attached to the
2199 // parent and as result doesn't remove itself from its children.
2200 uint32_t count = aAccessible->ContentChildCount();
2201 for (uint32_t idx = 0, jdx = 0; idx < count; idx++) {
2202 Accessible* child = aAccessible->ContentChildAt(jdx);
2203 if (!child->IsBoundToParent()) {
2204 NS_ERROR("Parent refers to a child, child doesn't refer to parent!");
2205 jdx++;
2206 }
2207
2208 // Don't cross document boundaries. The outerdoc shutdown takes care about
2209 // its subdocument.
2210 if (!child->IsDoc()) ShutdownChildrenInSubtree(child);
2211 }
2212
2213 UnbindFromDocument(aAccessible);
2214 }
2215
IsLoadEventTarget() const2216 bool DocAccessible::IsLoadEventTarget() const {
2217 nsCOMPtr<nsIDocShellTreeItem> treeItem = mDocumentNode->GetDocShell();
2218 NS_ASSERTION(treeItem, "No document shell for document!");
2219
2220 nsCOMPtr<nsIDocShellTreeItem> parentTreeItem;
2221 treeItem->GetParent(getter_AddRefs(parentTreeItem));
2222
2223 // Not a root document.
2224 if (parentTreeItem) {
2225 // Return true if it's either:
2226 // a) tab document;
2227 nsCOMPtr<nsIDocShellTreeItem> rootTreeItem;
2228 treeItem->GetRootTreeItem(getter_AddRefs(rootTreeItem));
2229 if (parentTreeItem == rootTreeItem) return true;
2230
2231 // b) frame/iframe document and its parent document is not in loading state
2232 // Note: we can get notifications while document is loading (and thus
2233 // while there's no parent document yet).
2234 DocAccessible* parentDoc = ParentDocument();
2235 return parentDoc && parentDoc->HasLoadState(eCompletelyLoaded);
2236 }
2237
2238 // It's content (not chrome) root document.
2239 return (treeItem->ItemType() == nsIDocShellTreeItem::typeContent);
2240 }
2241