|
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
3 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 |
|
6 #include "NotificationController.h" |
|
7 |
|
8 #include "DocAccessible-inl.h" |
|
9 #include "TextLeafAccessible.h" |
|
10 #include "TextUpdater.h" |
|
11 |
|
12 #include "mozilla/dom/Element.h" |
|
13 #include "mozilla/Telemetry.h" |
|
14 |
|
15 using namespace mozilla; |
|
16 using namespace mozilla::a11y; |
|
17 |
|
18 //////////////////////////////////////////////////////////////////////////////// |
|
19 // NotificationCollector |
|
20 //////////////////////////////////////////////////////////////////////////////// |
|
21 |
|
22 NotificationController::NotificationController(DocAccessible* aDocument, |
|
23 nsIPresShell* aPresShell) : |
|
24 EventQueue(aDocument), mObservingState(eNotObservingRefresh), |
|
25 mPresShell(aPresShell) |
|
26 { |
|
27 // Schedule initial accessible tree construction. |
|
28 ScheduleProcessing(); |
|
29 } |
|
30 |
|
31 NotificationController::~NotificationController() |
|
32 { |
|
33 NS_ASSERTION(!mDocument, "Controller wasn't shutdown properly!"); |
|
34 if (mDocument) |
|
35 Shutdown(); |
|
36 } |
|
37 |
|
38 //////////////////////////////////////////////////////////////////////////////// |
|
39 // NotificationCollector: AddRef/Release and cycle collection |
|
40 |
|
41 NS_IMPL_CYCLE_COLLECTING_NATIVE_ADDREF(NotificationController) |
|
42 NS_IMPL_CYCLE_COLLECTING_NATIVE_RELEASE(NotificationController) |
|
43 |
|
44 NS_IMPL_CYCLE_COLLECTION_CLASS(NotificationController) |
|
45 |
|
46 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(NotificationController) |
|
47 if (tmp->mDocument) |
|
48 tmp->Shutdown(); |
|
49 NS_IMPL_CYCLE_COLLECTION_UNLINK_END |
|
50 |
|
51 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(NotificationController) |
|
52 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHangingChildDocuments) |
|
53 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContentInsertions) |
|
54 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEvents) |
|
55 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END |
|
56 |
|
57 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(NotificationController, AddRef) |
|
58 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(NotificationController, Release) |
|
59 |
|
60 //////////////////////////////////////////////////////////////////////////////// |
|
61 // NotificationCollector: public |
|
62 |
|
63 void |
|
64 NotificationController::Shutdown() |
|
65 { |
|
66 if (mObservingState != eNotObservingRefresh && |
|
67 mPresShell->RemoveRefreshObserver(this, Flush_Display)) { |
|
68 mObservingState = eNotObservingRefresh; |
|
69 } |
|
70 |
|
71 // Shutdown handling child documents. |
|
72 int32_t childDocCount = mHangingChildDocuments.Length(); |
|
73 for (int32_t idx = childDocCount - 1; idx >= 0; idx--) { |
|
74 if (!mHangingChildDocuments[idx]->IsDefunct()) |
|
75 mHangingChildDocuments[idx]->Shutdown(); |
|
76 } |
|
77 |
|
78 mHangingChildDocuments.Clear(); |
|
79 |
|
80 mDocument = nullptr; |
|
81 mPresShell = nullptr; |
|
82 |
|
83 mTextHash.Clear(); |
|
84 mContentInsertions.Clear(); |
|
85 mNotifications.Clear(); |
|
86 mEvents.Clear(); |
|
87 } |
|
88 |
|
89 void |
|
90 NotificationController::ScheduleChildDocBinding(DocAccessible* aDocument) |
|
91 { |
|
92 // Schedule child document binding to the tree. |
|
93 mHangingChildDocuments.AppendElement(aDocument); |
|
94 ScheduleProcessing(); |
|
95 } |
|
96 |
|
97 void |
|
98 NotificationController::ScheduleContentInsertion(Accessible* aContainer, |
|
99 nsIContent* aStartChildNode, |
|
100 nsIContent* aEndChildNode) |
|
101 { |
|
102 nsRefPtr<ContentInsertion> insertion = new ContentInsertion(mDocument, |
|
103 aContainer); |
|
104 if (insertion && insertion->InitChildList(aStartChildNode, aEndChildNode) && |
|
105 mContentInsertions.AppendElement(insertion)) { |
|
106 ScheduleProcessing(); |
|
107 } |
|
108 } |
|
109 |
|
110 //////////////////////////////////////////////////////////////////////////////// |
|
111 // NotificationCollector: protected |
|
112 |
|
113 void |
|
114 NotificationController::ScheduleProcessing() |
|
115 { |
|
116 // If notification flush isn't planed yet start notification flush |
|
117 // asynchronously (after style and layout). |
|
118 if (mObservingState == eNotObservingRefresh) { |
|
119 if (mPresShell->AddRefreshObserver(this, Flush_Display)) |
|
120 mObservingState = eRefreshObserving; |
|
121 } |
|
122 } |
|
123 |
|
124 bool |
|
125 NotificationController::IsUpdatePending() |
|
126 { |
|
127 return mPresShell->IsLayoutFlushObserver() || |
|
128 mObservingState == eRefreshProcessingForUpdate || |
|
129 mContentInsertions.Length() != 0 || mNotifications.Length() != 0 || |
|
130 mTextHash.Count() != 0 || |
|
131 !mDocument->HasLoadState(DocAccessible::eTreeConstructed); |
|
132 } |
|
133 |
|
134 //////////////////////////////////////////////////////////////////////////////// |
|
135 // NotificationCollector: private |
|
136 |
|
137 void |
|
138 NotificationController::WillRefresh(mozilla::TimeStamp aTime) |
|
139 { |
|
140 Telemetry::AutoTimer<Telemetry::A11Y_UPDATE_TIME> updateTimer; |
|
141 |
|
142 // If the document accessible that notification collector was created for is |
|
143 // now shut down, don't process notifications anymore. |
|
144 NS_ASSERTION(mDocument, |
|
145 "The document was shut down while refresh observer is attached!"); |
|
146 if (!mDocument) |
|
147 return; |
|
148 |
|
149 if (mObservingState == eRefreshProcessing || |
|
150 mObservingState == eRefreshProcessingForUpdate) |
|
151 return; |
|
152 |
|
153 // Any generic notifications should be queued if we're processing content |
|
154 // insertions or generic notifications. |
|
155 mObservingState = eRefreshProcessingForUpdate; |
|
156 |
|
157 // Initial accessible tree construction. |
|
158 if (!mDocument->HasLoadState(DocAccessible::eTreeConstructed)) { |
|
159 // If document is not bound to parent at this point then the document is not |
|
160 // ready yet (process notifications later). |
|
161 if (!mDocument->IsBoundToParent()) { |
|
162 mObservingState = eRefreshObserving; |
|
163 return; |
|
164 } |
|
165 |
|
166 #ifdef A11Y_LOG |
|
167 if (logging::IsEnabled(logging::eTree)) { |
|
168 logging::MsgBegin("TREE", "initial tree created"); |
|
169 logging::Address("document", mDocument); |
|
170 logging::MsgEnd(); |
|
171 } |
|
172 #endif |
|
173 |
|
174 mDocument->DoInitialUpdate(); |
|
175 |
|
176 NS_ASSERTION(mContentInsertions.Length() == 0, |
|
177 "Pending content insertions while initial accessible tree isn't created!"); |
|
178 } |
|
179 |
|
180 // Initialize scroll support if needed. |
|
181 if (!(mDocument->mDocFlags & DocAccessible::eScrollInitialized)) |
|
182 mDocument->AddScrollListener(); |
|
183 |
|
184 // Process content inserted notifications to update the tree. Process other |
|
185 // notifications like DOM events and then flush event queue. If any new |
|
186 // notifications are queued during this processing then they will be processed |
|
187 // on next refresh. If notification processing queues up new events then they |
|
188 // are processed in this refresh. If events processing queues up new events |
|
189 // then new events are processed on next refresh. |
|
190 // Note: notification processing or event handling may shut down the owning |
|
191 // document accessible. |
|
192 |
|
193 // Process only currently queued content inserted notifications. |
|
194 nsTArray<nsRefPtr<ContentInsertion> > contentInsertions; |
|
195 contentInsertions.SwapElements(mContentInsertions); |
|
196 |
|
197 uint32_t insertionCount = contentInsertions.Length(); |
|
198 for (uint32_t idx = 0; idx < insertionCount; idx++) { |
|
199 contentInsertions[idx]->Process(); |
|
200 if (!mDocument) |
|
201 return; |
|
202 } |
|
203 |
|
204 // Process rendered text change notifications. |
|
205 mTextHash.EnumerateEntries(TextEnumerator, mDocument); |
|
206 mTextHash.Clear(); |
|
207 |
|
208 // Bind hanging child documents. |
|
209 uint32_t hangingDocCnt = mHangingChildDocuments.Length(); |
|
210 for (uint32_t idx = 0; idx < hangingDocCnt; idx++) { |
|
211 DocAccessible* childDoc = mHangingChildDocuments[idx]; |
|
212 if (childDoc->IsDefunct()) |
|
213 continue; |
|
214 |
|
215 nsIContent* ownerContent = mDocument->DocumentNode()-> |
|
216 FindContentForSubDocument(childDoc->DocumentNode()); |
|
217 if (ownerContent) { |
|
218 Accessible* outerDocAcc = mDocument->GetAccessible(ownerContent); |
|
219 if (outerDocAcc && outerDocAcc->AppendChild(childDoc)) { |
|
220 if (mDocument->AppendChildDocument(childDoc)) |
|
221 continue; |
|
222 |
|
223 outerDocAcc->RemoveChild(childDoc); |
|
224 } |
|
225 |
|
226 // Failed to bind the child document, destroy it. |
|
227 childDoc->Shutdown(); |
|
228 } |
|
229 } |
|
230 mHangingChildDocuments.Clear(); |
|
231 |
|
232 // If the document is ready and all its subdocuments are completely loaded |
|
233 // then process the document load. |
|
234 if (mDocument->HasLoadState(DocAccessible::eReady) && |
|
235 !mDocument->HasLoadState(DocAccessible::eCompletelyLoaded) && |
|
236 hangingDocCnt == 0) { |
|
237 uint32_t childDocCnt = mDocument->ChildDocumentCount(), childDocIdx = 0; |
|
238 for (; childDocIdx < childDocCnt; childDocIdx++) { |
|
239 DocAccessible* childDoc = mDocument->GetChildDocumentAt(childDocIdx); |
|
240 if (!childDoc->HasLoadState(DocAccessible::eCompletelyLoaded)) |
|
241 break; |
|
242 } |
|
243 |
|
244 if (childDocIdx == childDocCnt) { |
|
245 mDocument->ProcessLoad(); |
|
246 if (!mDocument) |
|
247 return; |
|
248 } |
|
249 } |
|
250 |
|
251 // Process only currently queued generic notifications. |
|
252 nsTArray < nsRefPtr<Notification> > notifications; |
|
253 notifications.SwapElements(mNotifications); |
|
254 |
|
255 uint32_t notificationCount = notifications.Length(); |
|
256 for (uint32_t idx = 0; idx < notificationCount; idx++) { |
|
257 notifications[idx]->Process(); |
|
258 if (!mDocument) |
|
259 return; |
|
260 } |
|
261 |
|
262 // Process invalidation list of the document after all accessible tree |
|
263 // modification are done. |
|
264 mDocument->ProcessInvalidationList(); |
|
265 |
|
266 // If a generic notification occurs after this point then we may be allowed to |
|
267 // process it synchronously. However we do not want to reenter if fireing |
|
268 // events causes script to run. |
|
269 mObservingState = eRefreshProcessing; |
|
270 |
|
271 ProcessEventQueue(); |
|
272 mObservingState = eRefreshObserving; |
|
273 if (!mDocument) |
|
274 return; |
|
275 |
|
276 // Stop further processing if there are no new notifications of any kind or |
|
277 // events and document load is processed. |
|
278 if (mContentInsertions.IsEmpty() && mNotifications.IsEmpty() && |
|
279 mEvents.IsEmpty() && mTextHash.Count() == 0 && |
|
280 mHangingChildDocuments.IsEmpty() && |
|
281 mDocument->HasLoadState(DocAccessible::eCompletelyLoaded) && |
|
282 mPresShell->RemoveRefreshObserver(this, Flush_Display)) { |
|
283 mObservingState = eNotObservingRefresh; |
|
284 } |
|
285 } |
|
286 |
|
287 //////////////////////////////////////////////////////////////////////////////// |
|
288 // Notification controller: text leaf accessible text update |
|
289 |
|
290 PLDHashOperator |
|
291 NotificationController::TextEnumerator(nsCOMPtrHashKey<nsIContent>* aEntry, |
|
292 void* aUserArg) |
|
293 { |
|
294 DocAccessible* document = static_cast<DocAccessible*>(aUserArg); |
|
295 nsIContent* textNode = aEntry->GetKey(); |
|
296 Accessible* textAcc = document->GetAccessible(textNode); |
|
297 |
|
298 // If the text node is not in tree or doesn't have frame then this case should |
|
299 // have been handled already by content removal notifications. |
|
300 nsINode* containerNode = textNode->GetParentNode(); |
|
301 if (!containerNode) { |
|
302 NS_ASSERTION(!textAcc, |
|
303 "Text node was removed but accessible is kept alive!"); |
|
304 return PL_DHASH_NEXT; |
|
305 } |
|
306 |
|
307 nsIFrame* textFrame = textNode->GetPrimaryFrame(); |
|
308 if (!textFrame) { |
|
309 NS_ASSERTION(!textAcc, |
|
310 "Text node isn't rendered but accessible is kept alive!"); |
|
311 return PL_DHASH_NEXT; |
|
312 } |
|
313 |
|
314 nsIContent* containerElm = containerNode->IsElement() ? |
|
315 containerNode->AsElement() : nullptr; |
|
316 |
|
317 nsAutoString text; |
|
318 textFrame->GetRenderedText(&text); |
|
319 |
|
320 // Remove text accessible if rendered text is empty. |
|
321 if (textAcc) { |
|
322 if (text.IsEmpty()) { |
|
323 #ifdef A11Y_LOG |
|
324 if (logging::IsEnabled(logging::eTree | logging::eText)) { |
|
325 logging::MsgBegin("TREE", "text node lost its content"); |
|
326 logging::Node("container", containerElm); |
|
327 logging::Node("content", textNode); |
|
328 logging::MsgEnd(); |
|
329 } |
|
330 #endif |
|
331 |
|
332 document->ContentRemoved(containerElm, textNode); |
|
333 return PL_DHASH_NEXT; |
|
334 } |
|
335 |
|
336 // Update text of the accessible and fire text change events. |
|
337 #ifdef A11Y_LOG |
|
338 if (logging::IsEnabled(logging::eText)) { |
|
339 logging::MsgBegin("TEXT", "text may be changed"); |
|
340 logging::Node("container", containerElm); |
|
341 logging::Node("content", textNode); |
|
342 logging::MsgEntry("old text '%s'", |
|
343 NS_ConvertUTF16toUTF8(textAcc->AsTextLeaf()->Text()).get()); |
|
344 logging::MsgEntry("new text: '%s'", |
|
345 NS_ConvertUTF16toUTF8(text).get()); |
|
346 logging::MsgEnd(); |
|
347 } |
|
348 #endif |
|
349 |
|
350 TextUpdater::Run(document, textAcc->AsTextLeaf(), text); |
|
351 return PL_DHASH_NEXT; |
|
352 } |
|
353 |
|
354 // Append an accessible if rendered text is not empty. |
|
355 if (!text.IsEmpty()) { |
|
356 #ifdef A11Y_LOG |
|
357 if (logging::IsEnabled(logging::eTree | logging::eText)) { |
|
358 logging::MsgBegin("TREE", "text node gains new content"); |
|
359 logging::Node("container", containerElm); |
|
360 logging::Node("content", textNode); |
|
361 logging::MsgEnd(); |
|
362 } |
|
363 #endif |
|
364 |
|
365 // Make sure the text node is in accessible document still. |
|
366 Accessible* container = document->GetAccessibleOrContainer(containerNode); |
|
367 NS_ASSERTION(container, |
|
368 "Text node having rendered text hasn't accessible document!"); |
|
369 if (container) { |
|
370 nsTArray<nsCOMPtr<nsIContent> > insertedContents; |
|
371 insertedContents.AppendElement(textNode); |
|
372 document->ProcessContentInserted(container, &insertedContents); |
|
373 } |
|
374 } |
|
375 |
|
376 return PL_DHASH_NEXT; |
|
377 } |
|
378 |
|
379 |
|
380 //////////////////////////////////////////////////////////////////////////////// |
|
381 // NotificationController: content inserted notification |
|
382 |
|
383 NotificationController::ContentInsertion:: |
|
384 ContentInsertion(DocAccessible* aDocument, Accessible* aContainer) : |
|
385 mDocument(aDocument), mContainer(aContainer) |
|
386 { |
|
387 } |
|
388 |
|
389 bool |
|
390 NotificationController::ContentInsertion:: |
|
391 InitChildList(nsIContent* aStartChildNode, nsIContent* aEndChildNode) |
|
392 { |
|
393 bool haveToUpdate = false; |
|
394 |
|
395 nsIContent* node = aStartChildNode; |
|
396 while (node != aEndChildNode) { |
|
397 // Notification triggers for content insertion even if no content was |
|
398 // actually inserted, check if the given content has a frame to discard |
|
399 // this case early. |
|
400 if (node->GetPrimaryFrame()) { |
|
401 if (mInsertedContent.AppendElement(node)) |
|
402 haveToUpdate = true; |
|
403 } |
|
404 |
|
405 node = node->GetNextSibling(); |
|
406 } |
|
407 |
|
408 return haveToUpdate; |
|
409 } |
|
410 |
|
411 NS_IMPL_CYCLE_COLLECTION(NotificationController::ContentInsertion, |
|
412 mContainer) |
|
413 |
|
414 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(NotificationController::ContentInsertion, |
|
415 AddRef) |
|
416 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(NotificationController::ContentInsertion, |
|
417 Release) |
|
418 |
|
419 void |
|
420 NotificationController::ContentInsertion::Process() |
|
421 { |
|
422 mDocument->ProcessContentInserted(mContainer, &mInsertedContent); |
|
423 |
|
424 mDocument = nullptr; |
|
425 mContainer = nullptr; |
|
426 mInsertedContent.Clear(); |
|
427 } |
|
428 |