|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 #include "FocusManager.h" |
|
6 |
|
7 #include "Accessible-inl.h" |
|
8 #include "AccIterator.h" |
|
9 #include "DocAccessible-inl.h" |
|
10 #include "nsAccessibilityService.h" |
|
11 #include "nsAccUtils.h" |
|
12 #include "nsEventShell.h" |
|
13 #include "Role.h" |
|
14 |
|
15 #include "nsFocusManager.h" |
|
16 #include "mozilla/EventStateManager.h" |
|
17 #include "mozilla/dom/Element.h" |
|
18 |
|
19 namespace mozilla { |
|
20 namespace a11y { |
|
21 |
|
22 FocusManager::FocusManager() |
|
23 { |
|
24 } |
|
25 |
|
26 FocusManager::~FocusManager() |
|
27 { |
|
28 } |
|
29 |
|
30 Accessible* |
|
31 FocusManager::FocusedAccessible() const |
|
32 { |
|
33 if (mActiveItem) |
|
34 return mActiveItem; |
|
35 |
|
36 nsINode* focusedNode = FocusedDOMNode(); |
|
37 if (focusedNode) { |
|
38 DocAccessible* doc = |
|
39 GetAccService()->GetDocAccessible(focusedNode->OwnerDoc()); |
|
40 return doc ? doc->GetAccessibleEvenIfNotInMapOrContainer(focusedNode) : nullptr; |
|
41 } |
|
42 |
|
43 return nullptr; |
|
44 } |
|
45 |
|
46 bool |
|
47 FocusManager::IsFocused(const Accessible* aAccessible) const |
|
48 { |
|
49 if (mActiveItem) |
|
50 return mActiveItem == aAccessible; |
|
51 |
|
52 nsINode* focusedNode = FocusedDOMNode(); |
|
53 if (focusedNode) { |
|
54 // XXX: Before getting an accessible for node having a DOM focus make sure |
|
55 // they belong to the same document because it can trigger unwanted document |
|
56 // accessible creation for temporary about:blank document. Without this |
|
57 // peculiarity we would end up with plain implementation based on |
|
58 // FocusedAccessible() method call. Make sure this issue is fixed in |
|
59 // bug 638465. |
|
60 if (focusedNode->OwnerDoc() == aAccessible->GetNode()->OwnerDoc()) { |
|
61 DocAccessible* doc = |
|
62 GetAccService()->GetDocAccessible(focusedNode->OwnerDoc()); |
|
63 return aAccessible == |
|
64 (doc ? doc->GetAccessibleEvenIfNotInMapOrContainer(focusedNode) : nullptr); |
|
65 } |
|
66 } |
|
67 return false; |
|
68 } |
|
69 |
|
70 bool |
|
71 FocusManager::IsFocusWithin(const Accessible* aContainer) const |
|
72 { |
|
73 Accessible* child = FocusedAccessible(); |
|
74 while (child) { |
|
75 if (child == aContainer) |
|
76 return true; |
|
77 |
|
78 child = child->Parent(); |
|
79 } |
|
80 return false; |
|
81 } |
|
82 |
|
83 FocusManager::FocusDisposition |
|
84 FocusManager::IsInOrContainsFocus(const Accessible* aAccessible) const |
|
85 { |
|
86 Accessible* focus = FocusedAccessible(); |
|
87 if (!focus) |
|
88 return eNone; |
|
89 |
|
90 // If focused. |
|
91 if (focus == aAccessible) |
|
92 return eFocused; |
|
93 |
|
94 // If contains the focus. |
|
95 Accessible* child = focus->Parent(); |
|
96 while (child) { |
|
97 if (child == aAccessible) |
|
98 return eContainsFocus; |
|
99 |
|
100 child = child->Parent(); |
|
101 } |
|
102 |
|
103 // If contained by focus. |
|
104 child = aAccessible->Parent(); |
|
105 while (child) { |
|
106 if (child == focus) |
|
107 return eContainedByFocus; |
|
108 |
|
109 child = child->Parent(); |
|
110 } |
|
111 |
|
112 return eNone; |
|
113 } |
|
114 |
|
115 void |
|
116 FocusManager::NotifyOfDOMFocus(nsISupports* aTarget) |
|
117 { |
|
118 #ifdef A11Y_LOG |
|
119 if (logging::IsEnabled(logging::eFocus)) |
|
120 logging::FocusNotificationTarget("DOM focus", "Target", aTarget); |
|
121 #endif |
|
122 |
|
123 mActiveItem = nullptr; |
|
124 |
|
125 nsCOMPtr<nsINode> targetNode(do_QueryInterface(aTarget)); |
|
126 if (targetNode) { |
|
127 DocAccessible* document = |
|
128 GetAccService()->GetDocAccessible(targetNode->OwnerDoc()); |
|
129 if (document) { |
|
130 // Set selection listener for focused element. |
|
131 if (targetNode->IsElement()) |
|
132 SelectionMgr()->SetControlSelectionListener(targetNode->AsElement()); |
|
133 |
|
134 document->HandleNotification<FocusManager, nsINode> |
|
135 (this, &FocusManager::ProcessDOMFocus, targetNode); |
|
136 } |
|
137 } |
|
138 } |
|
139 |
|
140 void |
|
141 FocusManager::NotifyOfDOMBlur(nsISupports* aTarget) |
|
142 { |
|
143 #ifdef A11Y_LOG |
|
144 if (logging::IsEnabled(logging::eFocus)) |
|
145 logging::FocusNotificationTarget("DOM blur", "Target", aTarget); |
|
146 #endif |
|
147 |
|
148 mActiveItem = nullptr; |
|
149 |
|
150 // If DOM document stays focused then fire accessible focus event to process |
|
151 // the case when no element within this DOM document will be focused. |
|
152 nsCOMPtr<nsINode> targetNode(do_QueryInterface(aTarget)); |
|
153 if (targetNode && targetNode->OwnerDoc() == FocusedDOMDocument()) { |
|
154 nsIDocument* DOMDoc = targetNode->OwnerDoc(); |
|
155 DocAccessible* document = |
|
156 GetAccService()->GetDocAccessible(DOMDoc); |
|
157 if (document) { |
|
158 // Clear selection listener for previously focused element. |
|
159 if (targetNode->IsElement()) |
|
160 SelectionMgr()->ClearControlSelectionListener(); |
|
161 |
|
162 document->HandleNotification<FocusManager, nsINode> |
|
163 (this, &FocusManager::ProcessDOMFocus, DOMDoc); |
|
164 } |
|
165 } |
|
166 } |
|
167 |
|
168 void |
|
169 FocusManager::ActiveItemChanged(Accessible* aItem, bool aCheckIfActive) |
|
170 { |
|
171 #ifdef A11Y_LOG |
|
172 if (logging::IsEnabled(logging::eFocus)) |
|
173 logging::FocusNotificationTarget("active item changed", "Item", aItem); |
|
174 #endif |
|
175 |
|
176 // Nothing changed, happens for XUL trees and HTML selects. |
|
177 if (aItem && aItem == mActiveItem) |
|
178 return; |
|
179 |
|
180 mActiveItem = nullptr; |
|
181 |
|
182 if (aItem && aCheckIfActive) { |
|
183 Accessible* widget = aItem->ContainerWidget(); |
|
184 #ifdef A11Y_LOG |
|
185 if (logging::IsEnabled(logging::eFocus)) |
|
186 logging::ActiveWidget(widget); |
|
187 #endif |
|
188 if (!widget || !widget->IsActiveWidget() || !widget->AreItemsOperable()) |
|
189 return; |
|
190 } |
|
191 mActiveItem = aItem; |
|
192 |
|
193 // If active item is changed then fire accessible focus event on it, otherwise |
|
194 // if there's no an active item then fire focus event to accessible having |
|
195 // DOM focus. |
|
196 Accessible* target = FocusedAccessible(); |
|
197 if (target) |
|
198 DispatchFocusEvent(target->Document(), target); |
|
199 } |
|
200 |
|
201 void |
|
202 FocusManager::ForceFocusEvent() |
|
203 { |
|
204 nsINode* focusedNode = FocusedDOMNode(); |
|
205 if (focusedNode) { |
|
206 DocAccessible* document = |
|
207 GetAccService()->GetDocAccessible(focusedNode->OwnerDoc()); |
|
208 if (document) { |
|
209 document->HandleNotification<FocusManager, nsINode> |
|
210 (this, &FocusManager::ProcessDOMFocus, focusedNode); |
|
211 } |
|
212 } |
|
213 } |
|
214 |
|
215 void |
|
216 FocusManager::DispatchFocusEvent(DocAccessible* aDocument, |
|
217 Accessible* aTarget) |
|
218 { |
|
219 NS_PRECONDITION(aDocument, "No document for focused accessible!"); |
|
220 if (aDocument) { |
|
221 nsRefPtr<AccEvent> event = |
|
222 new AccEvent(nsIAccessibleEvent::EVENT_FOCUS, aTarget, |
|
223 eAutoDetect, AccEvent::eCoalesceOfSameType); |
|
224 aDocument->FireDelayedEvent(event); |
|
225 |
|
226 #ifdef A11Y_LOG |
|
227 if (logging::IsEnabled(logging::eFocus)) |
|
228 logging::FocusDispatched(aTarget); |
|
229 #endif |
|
230 } |
|
231 } |
|
232 |
|
233 void |
|
234 FocusManager::ProcessDOMFocus(nsINode* aTarget) |
|
235 { |
|
236 #ifdef A11Y_LOG |
|
237 if (logging::IsEnabled(logging::eFocus)) |
|
238 logging::FocusNotificationTarget("process DOM focus", "Target", aTarget); |
|
239 #endif |
|
240 |
|
241 DocAccessible* document = |
|
242 GetAccService()->GetDocAccessible(aTarget->OwnerDoc()); |
|
243 if (!document) |
|
244 return; |
|
245 |
|
246 Accessible* target = document->GetAccessibleEvenIfNotInMapOrContainer(aTarget); |
|
247 if (target) { |
|
248 // Check if still focused. Otherwise we can end up with storing the active |
|
249 // item for control that isn't focused anymore. |
|
250 nsINode* focusedNode = FocusedDOMNode(); |
|
251 if (!focusedNode) |
|
252 return; |
|
253 |
|
254 Accessible* DOMFocus = |
|
255 document->GetAccessibleEvenIfNotInMapOrContainer(focusedNode); |
|
256 if (target != DOMFocus) |
|
257 return; |
|
258 |
|
259 Accessible* activeItem = target->CurrentItem(); |
|
260 if (activeItem) { |
|
261 mActiveItem = activeItem; |
|
262 target = activeItem; |
|
263 } |
|
264 |
|
265 DispatchFocusEvent(document, target); |
|
266 } |
|
267 } |
|
268 |
|
269 void |
|
270 FocusManager::ProcessFocusEvent(AccEvent* aEvent) |
|
271 { |
|
272 NS_PRECONDITION(aEvent->GetEventType() == nsIAccessibleEvent::EVENT_FOCUS, |
|
273 "Focus event is expected!"); |
|
274 |
|
275 // Emit focus event if event target is the active item. Otherwise then check |
|
276 // if it's still focused and then update active item and emit focus event. |
|
277 Accessible* target = aEvent->GetAccessible(); |
|
278 if (target != mActiveItem) { |
|
279 |
|
280 // Check if still focused. Otherwise we can end up with storing the active |
|
281 // item for control that isn't focused anymore. |
|
282 DocAccessible* document = aEvent->GetDocAccessible(); |
|
283 nsINode* focusedNode = FocusedDOMNode(); |
|
284 if (!focusedNode) |
|
285 return; |
|
286 |
|
287 Accessible* DOMFocus = |
|
288 document->GetAccessibleEvenIfNotInMapOrContainer(focusedNode); |
|
289 if (target != DOMFocus) |
|
290 return; |
|
291 |
|
292 Accessible* activeItem = target->CurrentItem(); |
|
293 if (activeItem) { |
|
294 mActiveItem = activeItem; |
|
295 target = activeItem; |
|
296 } |
|
297 } |
|
298 |
|
299 // Fire menu start/end events for ARIA menus. |
|
300 if (target->IsARIARole(nsGkAtoms::menuitem)) { |
|
301 // The focus was moved into menu. |
|
302 bool tryOwnsParent = true; |
|
303 Accessible* ARIAMenubar = nullptr; |
|
304 Accessible* child = target; |
|
305 Accessible* parent = child->Parent(); |
|
306 while (parent) { |
|
307 nsRoleMapEntry* roleMap = parent->ARIARoleMap(); |
|
308 if (roleMap) { |
|
309 if (roleMap->Is(nsGkAtoms::menubar)) { |
|
310 ARIAMenubar = parent; |
|
311 break; |
|
312 } |
|
313 |
|
314 // Go up in the parent chain of the menu hierarchy. |
|
315 if (roleMap->Is(nsGkAtoms::menuitem) || roleMap->Is(nsGkAtoms::menu)) { |
|
316 child = parent; |
|
317 parent = child->Parent(); |
|
318 tryOwnsParent = true; |
|
319 continue; |
|
320 } |
|
321 } |
|
322 |
|
323 // If no required context role then check aria-owns relation. |
|
324 if (!tryOwnsParent) |
|
325 break; |
|
326 |
|
327 RelatedAccIterator iter(child->Document(), child->GetContent(), |
|
328 nsGkAtoms::aria_owns); |
|
329 parent = iter.Next(); |
|
330 tryOwnsParent = false; |
|
331 } |
|
332 |
|
333 if (ARIAMenubar != mActiveARIAMenubar) { |
|
334 // Leaving ARIA menu. Fire menu_end event on current menubar. |
|
335 if (mActiveARIAMenubar) { |
|
336 nsRefPtr<AccEvent> menuEndEvent = |
|
337 new AccEvent(nsIAccessibleEvent::EVENT_MENU_END, mActiveARIAMenubar, |
|
338 aEvent->FromUserInput()); |
|
339 nsEventShell::FireEvent(menuEndEvent); |
|
340 } |
|
341 |
|
342 mActiveARIAMenubar = ARIAMenubar; |
|
343 |
|
344 // Entering ARIA menu. Fire menu_start event. |
|
345 if (mActiveARIAMenubar) { |
|
346 nsRefPtr<AccEvent> menuStartEvent = |
|
347 new AccEvent(nsIAccessibleEvent::EVENT_MENU_START, |
|
348 mActiveARIAMenubar, aEvent->FromUserInput()); |
|
349 nsEventShell::FireEvent(menuStartEvent); |
|
350 } |
|
351 } |
|
352 } else if (mActiveARIAMenubar) { |
|
353 // Focus left a menu. Fire menu_end event. |
|
354 nsRefPtr<AccEvent> menuEndEvent = |
|
355 new AccEvent(nsIAccessibleEvent::EVENT_MENU_END, mActiveARIAMenubar, |
|
356 aEvent->FromUserInput()); |
|
357 nsEventShell::FireEvent(menuEndEvent); |
|
358 |
|
359 mActiveARIAMenubar = nullptr; |
|
360 } |
|
361 |
|
362 #ifdef A11Y_LOG |
|
363 if (logging::IsEnabled(logging::eFocus)) |
|
364 logging::FocusNotificationTarget("fire focus event", "Target", target); |
|
365 #endif |
|
366 |
|
367 nsRefPtr<AccEvent> focusEvent = |
|
368 new AccEvent(nsIAccessibleEvent::EVENT_FOCUS, target, aEvent->FromUserInput()); |
|
369 nsEventShell::FireEvent(focusEvent); |
|
370 |
|
371 // Fire scrolling_start event when the document receives the focus if it has |
|
372 // an anchor jump. If an accessible within the document receive the focus |
|
373 // then null out the anchor jump because it no longer applies. |
|
374 DocAccessible* targetDocument = target->Document(); |
|
375 Accessible* anchorJump = targetDocument->AnchorJump(); |
|
376 if (anchorJump) { |
|
377 if (target == targetDocument) { |
|
378 // XXX: bug 625699, note in some cases the node could go away before we |
|
379 // we receive focus event, for example if the node is removed from DOM. |
|
380 nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_SCROLLING_START, |
|
381 anchorJump, aEvent->FromUserInput()); |
|
382 } |
|
383 targetDocument->SetAnchorJump(nullptr); |
|
384 } |
|
385 } |
|
386 |
|
387 nsINode* |
|
388 FocusManager::FocusedDOMNode() const |
|
389 { |
|
390 nsFocusManager* DOMFocusManager = nsFocusManager::GetFocusManager(); |
|
391 nsIContent* focusedElm = DOMFocusManager->GetFocusedContent(); |
|
392 |
|
393 // No focus on remote target elements like xul:browser having DOM focus and |
|
394 // residing in chrome process because it means an element in content process |
|
395 // keeps the focus. |
|
396 if (focusedElm) { |
|
397 if (EventStateManager::IsRemoteTarget(focusedElm)) { |
|
398 return nullptr; |
|
399 } |
|
400 return focusedElm; |
|
401 } |
|
402 |
|
403 // Otherwise the focus can be on DOM document. |
|
404 nsPIDOMWindow* focusedWnd = DOMFocusManager->GetFocusedWindow(); |
|
405 return focusedWnd ? focusedWnd->GetExtantDoc() : nullptr; |
|
406 } |
|
407 |
|
408 nsIDocument* |
|
409 FocusManager::FocusedDOMDocument() const |
|
410 { |
|
411 nsINode* focusedNode = FocusedDOMNode(); |
|
412 return focusedNode ? focusedNode->OwnerDoc() : nullptr; |
|
413 } |
|
414 |
|
415 } // namespace a11y |
|
416 } // namespace mozilla |