FocusManager.cpp (15138B)
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 "LocalAccessible-inl.h" 8 #include "DocAccessible-inl.h" 9 #include "nsAccessibilityService.h" 10 #include "nsEventShell.h" 11 12 #include "nsFocusManager.h" 13 #include "mozilla/a11y/DocAccessibleParent.h" 14 #include "mozilla/EventStateManager.h" 15 #include "mozilla/dom/Element.h" 16 #include "mozilla/dom/BrowsingContext.h" 17 #include "mozilla/dom/BrowserParent.h" 18 19 namespace mozilla { 20 namespace a11y { 21 22 FocusManager::FocusManager() {} 23 24 FocusManager::~FocusManager() {} 25 26 LocalAccessible* FocusManager::FocusedLocalAccessible() const { 27 MOZ_ASSERT(NS_IsMainThread()); 28 if (mActiveItem) { 29 if (mActiveItem->IsDefunct()) { 30 MOZ_ASSERT_UNREACHABLE("Stored active item is unbound from document"); 31 return nullptr; 32 } 33 34 return mActiveItem; 35 } 36 37 if (nsAccessibilityService::IsShutdown()) { 38 // We might try to get or create a DocAccessible below, which isn't safe (or 39 // useful) if the accessibility service is shutting down. 40 return nullptr; 41 } 42 43 nsINode* focusedNode = FocusedDOMNode(); 44 if (focusedNode) { 45 DocAccessible* doc = 46 GetAccService()->GetDocAccessible(focusedNode->OwnerDoc()); 47 return doc ? doc->GetAccessibleEvenIfNotInMapOrContainer(focusedNode) 48 : nullptr; 49 } 50 51 return nullptr; 52 } 53 54 Accessible* FocusManager::FocusedAccessible() const { 55 #if defined(ANDROID) 56 // It's not safe to call FocusedLocalAccessible() except on the main thread. 57 // Android might query RemoteAccessibles on the UI thread, which might call 58 // FocusedAccessible(). Never try to get the focused LocalAccessible in this 59 // case. 60 if (NS_IsMainThread()) { 61 if (Accessible* focusedAcc = FocusedLocalAccessible()) { 62 return focusedAcc; 63 } 64 } else { 65 nsAccessibilityService::GetAndroidMonitor().AssertCurrentThreadOwns(); 66 } 67 return mFocusedRemoteDoc ? mFocusedRemoteDoc->GetFocusedAcc() : nullptr; 68 #else 69 if (Accessible* focusedAcc = FocusedLocalAccessible()) { 70 return focusedAcc; 71 } 72 73 if (!XRE_IsParentProcess()) { 74 // DocAccessibleParent's don't exist in the content 75 // process, so we can't return anything useful if this 76 // is the case. 77 return nullptr; 78 } 79 80 nsFocusManager* focusManagerDOM = nsFocusManager::GetFocusManager(); 81 if (!focusManagerDOM) { 82 return nullptr; 83 } 84 85 // If we call GetFocusedBrowsingContext from the chrome process 86 // it returns the BrowsingContext for the focused _window_, which 87 // is not helpful here. Instead use GetFocusedBrowsingContextInChrome 88 // which returns the content BrowsingContext that has focus. 89 dom::BrowsingContext* focusedContext = 90 focusManagerDOM->GetFocusedBrowsingContextInChrome(); 91 92 DocAccessibleParent* focusedDoc = 93 DocAccessibleParent::GetFrom(focusedContext); 94 return focusedDoc ? focusedDoc->GetFocusedAcc() : nullptr; 95 #endif // defined(ANDROID) 96 } 97 98 bool FocusManager::IsFocusWithin(const Accessible* aContainer) const { 99 Accessible* child = FocusedAccessible(); 100 while (child) { 101 if (child == aContainer) return true; 102 103 child = child->Parent(); 104 } 105 return false; 106 } 107 108 FocusManager::FocusDisposition FocusManager::IsInOrContainsFocus( 109 const LocalAccessible* aAccessible) const { 110 LocalAccessible* focus = FocusedLocalAccessible(); 111 if (!focus) return eNone; 112 113 // If focused. 114 if (focus == aAccessible) return eFocused; 115 116 // If contains the focus. 117 LocalAccessible* child = focus->LocalParent(); 118 while (child) { 119 if (child == aAccessible) return eContainsFocus; 120 121 child = child->LocalParent(); 122 } 123 124 // If contained by focus. 125 child = aAccessible->LocalParent(); 126 while (child) { 127 if (child == focus) return eContainedByFocus; 128 129 child = child->LocalParent(); 130 } 131 132 return eNone; 133 } 134 135 bool FocusManager::WasLastFocused(const LocalAccessible* aAccessible) const { 136 return mLastFocus == aAccessible; 137 } 138 139 void FocusManager::NotifyOfDOMFocus(nsISupports* aTarget) { 140 #ifdef A11Y_LOG 141 if (logging::IsEnabled(logging::eFocus)) { 142 logging::FocusNotificationTarget("DOM focus", "Target", aTarget); 143 } 144 #endif 145 146 mActiveItem = nullptr; 147 148 nsCOMPtr<nsINode> targetNode(do_QueryInterface(aTarget)); 149 if (targetNode) { 150 DocAccessible* document = 151 GetAccService()->GetDocAccessible(targetNode->OwnerDoc()); 152 if (document) { 153 // Set selection listener for focused element. 154 if (targetNode->IsElement()) { 155 SelectionMgr()->SetControlSelectionListener(targetNode->AsElement()); 156 } 157 158 document->HandleNotification<FocusManager, nsINode>( 159 this, &FocusManager::ProcessDOMFocus, targetNode); 160 } 161 } 162 } 163 164 void FocusManager::NotifyOfDOMBlur(nsISupports* aTarget) { 165 #ifdef A11Y_LOG 166 if (logging::IsEnabled(logging::eFocus)) { 167 logging::FocusNotificationTarget("DOM blur", "Target", aTarget); 168 } 169 #endif 170 171 mActiveItem = nullptr; 172 173 // If DOM document stays focused then fire accessible focus event to process 174 // the case when no element within this DOM document will be focused. 175 nsCOMPtr<nsINode> targetNode(do_QueryInterface(aTarget)); 176 if (targetNode && targetNode->OwnerDoc() == FocusedDOMDocument()) { 177 dom::Document* DOMDoc = targetNode->OwnerDoc(); 178 DocAccessible* document = GetAccService()->GetDocAccessible(DOMDoc); 179 if (document) { 180 // Clear selection listener for previously focused element. 181 if (targetNode->IsElement()) { 182 SelectionMgr()->ClearControlSelectionListener(); 183 } 184 185 document->HandleNotification<FocusManager, nsINode>( 186 this, &FocusManager::ProcessDOMFocus, DOMDoc); 187 } 188 } 189 } 190 191 void FocusManager::ActiveItemChanged(LocalAccessible* aItem, 192 bool aCheckIfActive) { 193 #ifdef A11Y_LOG 194 if (logging::IsEnabled(logging::eFocus)) { 195 logging::FocusNotificationTarget("active item changed", "Item", aItem); 196 } 197 #endif 198 199 // Nothing changed, happens for XUL trees and HTML selects. 200 if (aItem && aItem == mActiveItem) { 201 return; 202 } 203 204 mActiveItem = nullptr; 205 206 if (aItem && aCheckIfActive) { 207 LocalAccessible* widget = aItem->ContainerWidget(); 208 #ifdef A11Y_LOG 209 if (logging::IsEnabled(logging::eFocus)) logging::ActiveWidget(widget); 210 #endif 211 if (!widget || !widget->IsActiveWidget() || !widget->AreItemsOperable()) { 212 return; 213 } 214 } 215 mActiveItem = aItem; 216 217 // If mActiveItem is null we may need to shift a11y focus back to a remote 218 // document. For example, when combobox popup is closed, then 219 // the focus should be moved back to the combobox. 220 if (!mActiveItem && XRE_IsParentProcess()) { 221 dom::BrowserParent* browser = dom::BrowserParent::GetFocused(); 222 if (browser) { 223 a11y::DocAccessibleParent* dap = browser->GetTopLevelDocAccessible(); 224 if (dap) { 225 (void)dap->SendRestoreFocus(); 226 } 227 } 228 } 229 230 // If active item is changed then fire accessible focus event on it, otherwise 231 // if there's no an active item then fire focus event to accessible having 232 // DOM focus. 233 LocalAccessible* target = FocusedLocalAccessible(); 234 if (target) { 235 DispatchFocusEvent(target->Document(), target); 236 } 237 } 238 239 void FocusManager::ForceFocusEvent() { 240 nsINode* focusedNode = FocusedDOMNode(); 241 if (focusedNode) { 242 DocAccessible* document = 243 GetAccService()->GetDocAccessible(focusedNode->OwnerDoc()); 244 if (document) { 245 document->HandleNotification<FocusManager, nsINode>( 246 this, &FocusManager::ProcessDOMFocus, focusedNode); 247 } 248 } 249 } 250 251 void FocusManager::DispatchFocusEvent(DocAccessible* aDocument, 252 LocalAccessible* aTarget) { 253 MOZ_ASSERT(aDocument, "No document for focused accessible!"); 254 if (aDocument) { 255 RefPtr<AccEvent> event = 256 new AccEvent(nsIAccessibleEvent::EVENT_FOCUS, aTarget, eAutoDetect, 257 AccEvent::eCoalesceOfSameType); 258 aDocument->FireDelayedEvent(event); 259 mLastFocus = aTarget; 260 if (mActiveItem != aTarget) { 261 // This new focus overrides the stored active item, so clear the active 262 // item. Among other things, the old active item might die. 263 mActiveItem = nullptr; 264 } 265 266 #ifdef A11Y_LOG 267 if (logging::IsEnabled(logging::eFocus)) logging::FocusDispatched(aTarget); 268 #endif 269 } 270 } 271 272 void FocusManager::ProcessDOMFocus(nsINode* aTarget) { 273 #ifdef A11Y_LOG 274 if (logging::IsEnabled(logging::eFocus)) { 275 logging::FocusNotificationTarget("process DOM focus", "Target", aTarget); 276 } 277 #endif 278 279 DocAccessible* document = 280 GetAccService()->GetDocAccessible(aTarget->OwnerDoc()); 281 if (!document) return; 282 283 LocalAccessible* target = 284 document->GetAccessibleEvenIfNotInMapOrContainer(aTarget); 285 if (target) { 286 // Check if still focused. Otherwise we can end up with storing the active 287 // item for control that isn't focused anymore. 288 nsINode* focusedNode = FocusedDOMNode(); 289 if (!focusedNode) return; 290 291 LocalAccessible* DOMFocus = 292 document->GetAccessibleEvenIfNotInMapOrContainer(focusedNode); 293 if (target != DOMFocus) return; 294 295 LocalAccessible* activeItem = target->CurrentItem(); 296 if (activeItem) { 297 mActiveItem = activeItem; 298 target = activeItem; 299 } 300 301 DispatchFocusEvent(document, target); 302 } 303 } 304 305 void FocusManager::ProcessFocusEvent(AccEvent* aEvent) { 306 MOZ_ASSERT(aEvent->GetEventType() == nsIAccessibleEvent::EVENT_FOCUS, 307 "Focus event is expected!"); 308 309 // Emit focus event if event target is the active item. Otherwise then check 310 // if it's still focused and then update active item and emit focus event. 311 LocalAccessible* target = aEvent->GetAccessible(); 312 MOZ_ASSERT(!target->IsDefunct()); 313 if (target != mActiveItem) { 314 // Check if still focused. Otherwise we can end up with storing the active 315 // item for control that isn't focused anymore. 316 DocAccessible* document = aEvent->Document(); 317 nsINode* focusedNode = FocusedDOMNode(); 318 if (!focusedNode) return; 319 320 LocalAccessible* DOMFocus = 321 document->GetAccessibleEvenIfNotInMapOrContainer(focusedNode); 322 if (target != DOMFocus) return; 323 324 LocalAccessible* activeItem = target->CurrentItem(); 325 if (activeItem) { 326 mActiveItem = activeItem; 327 target = activeItem; 328 MOZ_ASSERT(!target->IsDefunct()); 329 } 330 } 331 332 // Fire menu start/end events for ARIA menus. 333 if (target->IsARIARole(nsGkAtoms::menuitem)) { 334 // The focus was moved into menu. 335 LocalAccessible* ARIAMenubar = nullptr; 336 for (LocalAccessible* parent = target->LocalParent(); parent; 337 parent = parent->LocalParent()) { 338 if (parent->IsARIARole(nsGkAtoms::menubar)) { 339 ARIAMenubar = parent; 340 break; 341 } 342 343 // Go up in the parent chain of the menu hierarchy. 344 if (!parent->IsARIARole(nsGkAtoms::menuitem) && 345 !parent->IsARIARole(nsGkAtoms::menu)) { 346 break; 347 } 348 } 349 350 if (ARIAMenubar != mActiveARIAMenubar) { 351 // Leaving ARIA menu. Fire menu_end event on current menubar. 352 if (mActiveARIAMenubar) { 353 RefPtr<AccEvent> menuEndEvent = 354 new AccEvent(nsIAccessibleEvent::EVENT_MENU_END, mActiveARIAMenubar, 355 aEvent->FromUserInput()); 356 nsEventShell::FireEvent(menuEndEvent); 357 } 358 359 mActiveARIAMenubar = ARIAMenubar; 360 361 // Entering ARIA menu. Fire menu_start event. 362 if (mActiveARIAMenubar) { 363 RefPtr<AccEvent> menuStartEvent = 364 new AccEvent(nsIAccessibleEvent::EVENT_MENU_START, 365 mActiveARIAMenubar, aEvent->FromUserInput()); 366 nsEventShell::FireEvent(menuStartEvent); 367 } 368 } 369 } else if (mActiveARIAMenubar) { 370 // Focus left a menu. Fire menu_end event. 371 RefPtr<AccEvent> menuEndEvent = 372 new AccEvent(nsIAccessibleEvent::EVENT_MENU_END, mActiveARIAMenubar, 373 aEvent->FromUserInput()); 374 nsEventShell::FireEvent(menuEndEvent); 375 376 mActiveARIAMenubar = nullptr; 377 } 378 379 #ifdef A11Y_LOG 380 if (logging::IsEnabled(logging::eFocus)) { 381 logging::FocusNotificationTarget("fire focus event", "Target", target); 382 } 383 #endif 384 385 // Reset cached caret value. The cache will be updated upon processing the 386 // next caret move event. This ensures that we will return the correct caret 387 // offset before the caret move event is handled. 388 SelectionMgr()->ResetCaretOffset(); 389 390 RefPtr<AccEvent> focusEvent = new AccEvent(nsIAccessibleEvent::EVENT_FOCUS, 391 target, aEvent->FromUserInput()); 392 nsEventShell::FireEvent(focusEvent); 393 394 if (NS_WARN_IF(target->IsDefunct())) { 395 // target died during nsEventShell::FireEvent. 396 return; 397 } 398 399 // An anchor jump may have occurred while the document was not focused. This 400 // could even be before the document was first ever focused. Process it now. 401 DocAccessible* targetDocument = target->Document(); 402 if (!targetDocument->ProcessAnchorJump()) { 403 // The anchor jump was ignored due to the focus. That means that this focus 404 // change overrides the anchor jump, so clear the jump. 405 targetDocument->SetAnchorJump(nullptr); 406 } 407 } 408 409 nsINode* FocusManager::FocusedDOMNode() const { 410 nsFocusManager* DOMFocusManager = nsFocusManager::GetFocusManager(); 411 nsIContent* focusedElm = DOMFocusManager->GetFocusedElement(); 412 nsIFrame* focusedFrame = focusedElm ? focusedElm->GetPrimaryFrame() : nullptr; 413 // DOM elements retain their focused state when they get styled as display: 414 // none/content or visibility: hidden. We should treat those cases as if those 415 // elements were removed, and focus on doc. 416 if (focusedFrame && focusedFrame->StyleVisibility()->IsVisible()) { 417 // Print preview documents don't get DocAccessibles, but we still want a11y 418 // focus to go somewhere useful. Therefore, we allow a11y focus to land on 419 // the OuterDocAccessible in this case. 420 // Note that this code only handles remote print preview documents. 421 if (EventStateManager::IsTopLevelRemoteTarget(focusedElm) && 422 focusedElm->AsElement()->HasAttribute(u"printpreview"_ns)) { 423 return focusedElm; 424 } 425 // No focus on remote target elements like xul:browser having DOM focus and 426 // residing in chrome process because it means an element in content process 427 // keeps the focus. Similarly, suppress focus on OOP iframes because an 428 // element in another content process should now have the focus. 429 if (EventStateManager::IsRemoteTarget(focusedElm)) { 430 return nullptr; 431 } 432 return focusedElm; 433 } 434 435 // Otherwise the focus can be on DOM document. 436 dom::BrowsingContext* context = DOMFocusManager->GetFocusedBrowsingContext(); 437 if (context) { 438 // GetDocShell will return null if the document isn't in our process. 439 nsIDocShell* shell = context->GetDocShell(); 440 if (shell) { 441 return shell->GetDocument(); 442 } 443 } 444 445 // Focus isn't in this process. 446 return nullptr; 447 } 448 449 dom::Document* FocusManager::FocusedDOMDocument() const { 450 nsINode* focusedNode = FocusedDOMNode(); 451 return focusedNode ? focusedNode->OwnerDoc() : nullptr; 452 } 453 454 } // namespace a11y 455 } // namespace mozilla