/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/dom/BrowserParent.h" #include "nsFocusManager.h" #include "LayoutConstants.h" #include "ChildIterator.h" #include "nsIInterfaceRequestorUtils.h" #include "nsGkAtoms.h" #include "nsGlobalWindow.h" #include "nsContentUtils.h" #include "ContentParent.h" #include "nsPIDOMWindow.h" #include "nsIContentInlines.h" #include "nsIDocShell.h" #include "nsIDocShellTreeOwner.h" #include "nsIFormControl.h" #include "nsLayoutUtils.h" #include "nsFrameTraversal.h" #include "nsIWebNavigation.h" #include "nsCaret.h" #include "nsIBaseWindow.h" #include "nsIAppWindow.h" #include "nsTextControlFrame.h" #include "nsViewManager.h" #include "nsFrameSelection.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/Selection.h" #include "nsXULPopupManager.h" #include "nsMenuPopupFrame.h" #include "nsIScriptError.h" #include "nsIScriptObjectPrincipal.h" #include "nsIPrincipal.h" #include "nsIObserverService.h" #include "BrowserChild.h" #include "nsFrameLoader.h" #include "nsHTMLDocument.h" #include "nsNetUtil.h" #include "nsRange.h" #include "nsFrameLoaderOwner.h" #include "nsQueryObject.h" #include "mozilla/AccessibleCaretEventHub.h" #include "mozilla/ContentEvents.h" #include "mozilla/dom/ContentChild.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/ElementBinding.h" #include "mozilla/dom/HTMLImageElement.h" #include "mozilla/dom/HTMLInputElement.h" #include "mozilla/dom/HTMLSlotElement.h" #include "mozilla/dom/BrowserBridgeChild.h" #include "mozilla/dom/Text.h" #include "mozilla/dom/XULPopupElement.h" #include "mozilla/dom/WindowGlobalParent.h" #include "mozilla/dom/WindowGlobalChild.h" #include "mozilla/EventDispatcher.h" #include "mozilla/EventStateManager.h" #include "mozilla/HTMLEditor.h" #include "mozilla/IMEStateManager.h" #include "mozilla/LookAndFeel.h" #include "mozilla/Maybe.h" #include "mozilla/PointerLockManager.h" #include "mozilla/Preferences.h" #include "mozilla/PresShell.h" #include "mozilla/Services.h" #include "mozilla/Unused.h" #include "mozilla/StaticPrefs_full_screen_api.h" #include #include "nsIDOMXULMenuListElement.h" #ifdef ACCESSIBILITY # include "nsAccessibilityService.h" #endif using namespace mozilla; using namespace mozilla::dom; using namespace mozilla::widget; // Two types of focus pr logging are available: // 'Focus' for normal focus manager calls // 'FocusNavigation' for tab and document navigation LazyLogModule gFocusLog("Focus"); LazyLogModule gFocusNavigationLog("FocusNavigation"); #define LOGFOCUS(args) MOZ_LOG(gFocusLog, mozilla::LogLevel::Debug, args) #define LOGFOCUSNAVIGATION(args) \ MOZ_LOG(gFocusNavigationLog, mozilla::LogLevel::Debug, args) #define LOGTAG(log, format, content) \ if (MOZ_LOG_TEST(log, LogLevel::Debug)) { \ nsAutoCString tag("(none)"_ns); \ if (content) { \ content->NodeInfo()->NameAtom()->ToUTF8String(tag); \ } \ MOZ_LOG(log, LogLevel::Debug, (format, tag.get())); \ } #define LOGCONTENT(format, content) LOGTAG(gFocusLog, format, content) #define LOGCONTENTNAVIGATION(format, content) \ LOGTAG(gFocusNavigationLog, format, content) struct nsDelayedBlurOrFocusEvent { nsDelayedBlurOrFocusEvent(EventMessage aEventMessage, PresShell* aPresShell, Document* aDocument, EventTarget* aTarget, EventTarget* aRelatedTarget) : mPresShell(aPresShell), mDocument(aDocument), mTarget(aTarget), mEventMessage(aEventMessage), mRelatedTarget(aRelatedTarget) {} nsDelayedBlurOrFocusEvent(const nsDelayedBlurOrFocusEvent& aOther) : mPresShell(aOther.mPresShell), mDocument(aOther.mDocument), mTarget(aOther.mTarget), mEventMessage(aOther.mEventMessage) {} RefPtr mPresShell; nsCOMPtr mDocument; nsCOMPtr mTarget; EventMessage mEventMessage; nsCOMPtr mRelatedTarget; }; inline void ImplCycleCollectionUnlink(nsDelayedBlurOrFocusEvent& aField) { aField.mPresShell = nullptr; aField.mDocument = nullptr; aField.mTarget = nullptr; aField.mRelatedTarget = nullptr; } inline void ImplCycleCollectionTraverse( nsCycleCollectionTraversalCallback& aCallback, nsDelayedBlurOrFocusEvent& aField, const char* aName, uint32_t aFlags = 0) { CycleCollectionNoteChild( aCallback, static_cast(aField.mPresShell.get()), aName, aFlags); CycleCollectionNoteChild(aCallback, aField.mDocument.get(), aName, aFlags); CycleCollectionNoteChild(aCallback, aField.mTarget.get(), aName, aFlags); CycleCollectionNoteChild(aCallback, aField.mRelatedTarget.get(), aName, aFlags); } NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsFocusManager) NS_INTERFACE_MAP_ENTRY(nsIFocusManager) NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIFocusManager) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(nsFocusManager) NS_IMPL_CYCLE_COLLECTING_RELEASE(nsFocusManager) NS_IMPL_CYCLE_COLLECTION_WEAK(nsFocusManager, mActiveWindow, mActiveBrowsingContextInContent, mActiveBrowsingContextInChrome, mFocusedWindow, mFocusedBrowsingContextInContent, mFocusedBrowsingContextInChrome, mFocusedElement, mFirstBlurEvent, mFirstFocusEvent, mWindowBeingLowered, mDelayedBlurFocusEvents) StaticRefPtr nsFocusManager::sInstance; bool nsFocusManager::sTestMode = false; uint64_t nsFocusManager::sFocusActionCounter = 0; static const char* kObservedPrefs[] = {"accessibility.browsewithcaret", "accessibility.tabfocus_applies_to_xul", "focusmanager.testmode", nullptr}; nsFocusManager::nsFocusManager() : mActionIdForActiveBrowsingContextInContent(0), mActionIdForActiveBrowsingContextInChrome(0), mActionIdForFocusedBrowsingContextInContent(0), mActionIdForFocusedBrowsingContextInChrome(0), mActiveBrowsingContextInContentSetFromOtherProcess(false), mEventHandlingNeedsFlush(false) {} nsFocusManager::~nsFocusManager() { Preferences::UnregisterCallbacks(nsFocusManager::PrefChanged, kObservedPrefs, this); nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->RemoveObserver(this, "xpcom-shutdown"); } } // static nsresult nsFocusManager::Init() { sInstance = new nsFocusManager(); nsIContent::sTabFocusModelAppliesToXUL = Preferences::GetBool("accessibility.tabfocus_applies_to_xul", nsIContent::sTabFocusModelAppliesToXUL); sTestMode = Preferences::GetBool("focusmanager.testmode", false); Preferences::RegisterCallbacks(nsFocusManager::PrefChanged, kObservedPrefs, sInstance.get()); nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->AddObserver(sInstance, "xpcom-shutdown", true); } return NS_OK; } // static void nsFocusManager::Shutdown() { sInstance = nullptr; } // static void nsFocusManager::PrefChanged(const char* aPref, void* aSelf) { if (RefPtr fm = static_cast(aSelf)) { fm->PrefChanged(aPref); } } void nsFocusManager::PrefChanged(const char* aPref) { nsDependentCString pref(aPref); if (pref.EqualsLiteral("accessibility.browsewithcaret")) { UpdateCaretForCaretBrowsingMode(); } else if (pref.EqualsLiteral("accessibility.tabfocus_applies_to_xul")) { nsIContent::sTabFocusModelAppliesToXUL = Preferences::GetBool("accessibility.tabfocus_applies_to_xul", nsIContent::sTabFocusModelAppliesToXUL); } else if (pref.EqualsLiteral("focusmanager.testmode")) { sTestMode = Preferences::GetBool("focusmanager.testmode", false); } } NS_IMETHODIMP nsFocusManager::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (!nsCRT::strcmp(aTopic, "xpcom-shutdown")) { mActiveWindow = nullptr; mActiveBrowsingContextInContent = nullptr; mActionIdForActiveBrowsingContextInContent = 0; mActionIdForFocusedBrowsingContextInContent = 0; mActiveBrowsingContextInChrome = nullptr; mActionIdForActiveBrowsingContextInChrome = 0; mActionIdForFocusedBrowsingContextInChrome = 0; mFocusedWindow = nullptr; mFocusedBrowsingContextInContent = nullptr; mFocusedBrowsingContextInChrome = nullptr; mFocusedElement = nullptr; mFirstBlurEvent = nullptr; mFirstFocusEvent = nullptr; mWindowBeingLowered = nullptr; mDelayedBlurFocusEvents.Clear(); } return NS_OK; } static bool ActionIdComparableAndLower(uint64_t aActionId, uint64_t aReference) { MOZ_ASSERT(aActionId, "Uninitialized action id"); auto [actionProc, actionId] = nsContentUtils::SplitProcessSpecificId(aActionId); auto [refProc, refId] = nsContentUtils::SplitProcessSpecificId(aReference); return actionProc == refProc && actionId < refId; } // given a frame content node, retrieve the nsIDOMWindow displayed in it static nsPIDOMWindowOuter* GetContentWindow(nsIContent* aContent) { Document* doc = aContent->GetComposedDoc(); if (doc) { Document* subdoc = doc->GetSubDocumentFor(aContent); if (subdoc) { return subdoc->GetWindow(); } } return nullptr; } bool nsFocusManager::IsFocused(nsIContent* aContent) { if (!aContent || !mFocusedElement) { return false; } return aContent == mFocusedElement; } bool nsFocusManager::IsTestMode() { return sTestMode; } bool nsFocusManager::IsInActiveWindow(BrowsingContext* aBC) const { RefPtr top = aBC->Top(); if (XRE_IsParentProcess()) { top = top->Canonical()->TopCrossChromeBoundary(); } return IsSameOrAncestor(top, GetActiveBrowsingContext()); } // get the current window for the given content node static nsPIDOMWindowOuter* GetCurrentWindow(nsIContent* aContent) { Document* doc = aContent->GetComposedDoc(); return doc ? doc->GetWindow() : nullptr; } // static Element* nsFocusManager::GetFocusedDescendant( nsPIDOMWindowOuter* aWindow, SearchRange aSearchRange, nsPIDOMWindowOuter** aFocusedWindow) { NS_ENSURE_TRUE(aWindow, nullptr); *aFocusedWindow = nullptr; Element* currentElement = nullptr; nsPIDOMWindowOuter* window = aWindow; for (;;) { *aFocusedWindow = window; currentElement = window->GetFocusedElement(); if (!currentElement || aSearchRange == eOnlyCurrentWindow) { break; } window = GetContentWindow(currentElement); if (!window) { break; } if (aSearchRange == eIncludeAllDescendants) { continue; } MOZ_ASSERT(aSearchRange == eIncludeVisibleDescendants); // If the child window doesn't have PresShell, it means the window is // invisible. nsIDocShell* docShell = window->GetDocShell(); if (!docShell) { break; } if (!docShell->GetPresShell()) { break; } } NS_IF_ADDREF(*aFocusedWindow); return currentElement; } // static InputContextAction::Cause nsFocusManager::GetFocusMoveActionCause( uint32_t aFlags) { if (aFlags & nsIFocusManager::FLAG_BYTOUCH) { return InputContextAction::CAUSE_TOUCH; } else if (aFlags & nsIFocusManager::FLAG_BYMOUSE) { return InputContextAction::CAUSE_MOUSE; } else if (aFlags & nsIFocusManager::FLAG_BYKEY) { return InputContextAction::CAUSE_KEY; } else if (aFlags & nsIFocusManager::FLAG_BYLONGPRESS) { return InputContextAction::CAUSE_LONGPRESS; } return InputContextAction::CAUSE_UNKNOWN; } NS_IMETHODIMP nsFocusManager::GetActiveWindow(mozIDOMWindowProxy** aWindow) { MOZ_ASSERT(XRE_IsParentProcess(), "Must not be called outside the parent process."); NS_IF_ADDREF(*aWindow = mActiveWindow); return NS_OK; } NS_IMETHODIMP nsFocusManager::GetActiveBrowsingContext(BrowsingContext** aBrowsingContext) { NS_IF_ADDREF(*aBrowsingContext = GetActiveBrowsingContext()); return NS_OK; } void nsFocusManager::FocusWindow(nsPIDOMWindowOuter* aWindow, CallerType aCallerType) { if (RefPtr fm = sInstance) { fm->SetFocusedWindowWithCallerType(aWindow, aCallerType); } } NS_IMETHODIMP nsFocusManager::GetFocusedWindow(mozIDOMWindowProxy** aFocusedWindow) { NS_IF_ADDREF(*aFocusedWindow = mFocusedWindow); return NS_OK; } NS_IMETHODIMP nsFocusManager::GetFocusedContentBrowsingContext( BrowsingContext** aBrowsingContext) { MOZ_DIAGNOSTIC_ASSERT( XRE_IsParentProcess(), "We only have use cases for this in the parent process"); NS_IF_ADDREF(*aBrowsingContext = GetFocusedBrowsingContextInChrome()); return NS_OK; } nsresult nsFocusManager::SetFocusedWindowWithCallerType( mozIDOMWindowProxy* aWindowToFocus, CallerType aCallerType) { LOGFOCUS(("<>")); nsCOMPtr windowToFocus = nsPIDOMWindowOuter::From(aWindowToFocus); NS_ENSURE_TRUE(windowToFocus, NS_ERROR_FAILURE); nsCOMPtr frameElement = windowToFocus->GetFrameElementInternal(); Maybe actionIdFromSetFocusInner; if (frameElement) { // pass false for aFocusChanged so that the caret does not get updated // and scrolling does not occur. actionIdFromSetFocusInner = SetFocusInner(frameElement, 0, false, true); } else { // this is a top-level window. If the window has a child frame focused, // clear the focus. Otherwise, focus should already be in this frame, or // already cleared. This ensures that focus will be in this frame and not // in a child. nsIContent* content = windowToFocus->GetFocusedElement(); if (content) { if (nsCOMPtr childWindow = GetContentWindow(content)) ClearFocus(windowToFocus); } } nsCOMPtr rootWindow = windowToFocus->GetPrivateRoot(); const uint64_t actionId = actionIdFromSetFocusInner.isSome() ? actionIdFromSetFocusInner.value() : sInstance->GenerateFocusActionId(); if (rootWindow) { RaiseWindow(rootWindow, aCallerType, actionId); } LOGFOCUS(("<>", actionId)); return NS_OK; } NS_IMETHODIMP nsFocusManager::SetFocusedWindow( mozIDOMWindowProxy* aWindowToFocus) { return SetFocusedWindowWithCallerType(aWindowToFocus, CallerType::System); } NS_IMETHODIMP nsFocusManager::GetFocusedElement(Element** aFocusedElement) { RefPtr focusedElement = mFocusedElement; focusedElement.forget(aFocusedElement); return NS_OK; } uint32_t nsFocusManager::GetLastFocusMethod(nsPIDOMWindowOuter* aWindow) const { nsPIDOMWindowOuter* window = aWindow ? aWindow : mFocusedWindow.get(); uint32_t method = window ? window->GetFocusMethod() : 0; NS_ASSERTION((method & METHOD_MASK) == method, "invalid focus method"); return method; } NS_IMETHODIMP nsFocusManager::GetLastFocusMethod(mozIDOMWindowProxy* aWindow, uint32_t* aLastFocusMethod) { *aLastFocusMethod = GetLastFocusMethod(nsPIDOMWindowOuter::From(aWindow)); return NS_OK; } NS_IMETHODIMP nsFocusManager::SetFocus(Element* aElement, uint32_t aFlags) { LOGFOCUS(("<>")); NS_ENSURE_ARG(aElement); SetFocusInner(aElement, aFlags, true, true); LOGFOCUS(("<>")); return NS_OK; } NS_IMETHODIMP nsFocusManager::ElementIsFocusable(Element* aElement, uint32_t aFlags, bool* aIsFocusable) { NS_ENSURE_TRUE(aElement, NS_ERROR_INVALID_ARG); *aIsFocusable = !!FlushAndCheckIfFocusable(aElement, aFlags); return NS_OK; } MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsFocusManager::MoveFocus(mozIDOMWindowProxy* aWindow, Element* aStartElement, uint32_t aType, uint32_t aFlags, Element** aElement) { *aElement = nullptr; LOGFOCUS(("<>", aType, aFlags)); if (MOZ_LOG_TEST(gFocusLog, LogLevel::Debug) && mFocusedWindow) { Document* doc = mFocusedWindow->GetExtantDoc(); if (doc && doc->GetDocumentURI()) { LOGFOCUS((" Focused Window: %p %s", mFocusedWindow.get(), doc->GetDocumentURI()->GetSpecOrDefault().get())); } } LOGCONTENT(" Current Focus: %s", mFocusedElement.get()); // use FLAG_BYMOVEFOCUS when switching focus with MoveFocus unless one of // the other focus methods is already set, or we're just moving to the root // or caret position. if (aType != MOVEFOCUS_ROOT && aType != MOVEFOCUS_CARET && (aFlags & METHOD_MASK) == 0) { aFlags |= FLAG_BYMOVEFOCUS; } nsCOMPtr window; if (aStartElement) { window = GetCurrentWindow(aStartElement); } else { window = aWindow ? nsPIDOMWindowOuter::From(aWindow) : mFocusedWindow.get(); } NS_ENSURE_TRUE(window, NS_ERROR_FAILURE); // Flush to ensure that focusability of descendants is computed correctly. if (RefPtr doc = window->GetExtantDoc()) { doc->FlushPendingNotifications(FlushType::EnsurePresShellInitAndFrames); } bool noParentTraversal = aFlags & FLAG_NOPARENTFRAME; nsCOMPtr newFocus; nsresult rv = DetermineElementToMoveFocus(window, aStartElement, aType, noParentTraversal, true, getter_AddRefs(newFocus)); if (rv == NS_SUCCESS_DOM_NO_OPERATION) { return NS_OK; } NS_ENSURE_SUCCESS(rv, rv); LOGCONTENTNAVIGATION("Element to be focused: %s", newFocus.get()); if (newFocus && newFocus->IsElement()) { // for caret movement, pass false for the aFocusChanged argument, // otherwise the caret will end up moving to the focus position. This // would be a problem because the caret would move to the beginning of the // focused link making it impossible to navigate the caret over a link. SetFocusInner(MOZ_KnownLive(newFocus->AsElement()), aFlags, aType != MOVEFOCUS_CARET, true); *aElement = do_AddRef(newFocus->AsElement()).take(); } else if (aType == MOVEFOCUS_ROOT || aType == MOVEFOCUS_CARET) { // no content was found, so clear the focus for these two types. ClearFocus(window); } LOGFOCUS(("<>")); return NS_OK; } NS_IMETHODIMP nsFocusManager::ClearFocus(mozIDOMWindowProxy* aWindow) { LOGFOCUS(("<>")); // if the window to clear is the focused window or an ancestor of the // focused window, then blur the existing focused content. Otherwise, the // focus is somewhere else so just update the current node. NS_ENSURE_TRUE(aWindow, NS_ERROR_INVALID_ARG); nsCOMPtr window = nsPIDOMWindowOuter::From(aWindow); if (IsSameOrAncestor(window, GetFocusedBrowsingContext())) { RefPtr bc = window->GetBrowsingContext(); bool isAncestor = (GetFocusedBrowsingContext() != bc); uint64_t actionId = GenerateFocusActionId(); if (Blur(bc, nullptr, isAncestor, true, false, actionId)) { // if we are clearing the focus on an ancestor of the focused window, // the ancestor will become the new focused window, so focus it if (isAncestor) { Focus(window, nullptr, 0, true, false, false, true, actionId); } } } else { window->SetFocusedElement(nullptr); } LOGFOCUS(("<>")); return NS_OK; } NS_IMETHODIMP nsFocusManager::GetFocusedElementForWindow(mozIDOMWindowProxy* aWindow, bool aDeep, mozIDOMWindowProxy** aFocusedWindow, Element** aElement) { *aElement = nullptr; if (aFocusedWindow) { *aFocusedWindow = nullptr; } NS_ENSURE_TRUE(aWindow, NS_ERROR_INVALID_ARG); nsCOMPtr window = nsPIDOMWindowOuter::From(aWindow); nsCOMPtr focusedWindow; RefPtr focusedElement = GetFocusedDescendant(window, aDeep ? nsFocusManager::eIncludeAllDescendants : nsFocusManager::eOnlyCurrentWindow, getter_AddRefs(focusedWindow)); focusedElement.forget(aElement); if (aFocusedWindow) { NS_IF_ADDREF(*aFocusedWindow = focusedWindow); } return NS_OK; } NS_IMETHODIMP nsFocusManager::MoveCaretToFocus(mozIDOMWindowProxy* aWindow) { nsCOMPtr webnav = do_GetInterface(aWindow); nsCOMPtr dsti = do_QueryInterface(webnav); if (dsti) { if (dsti->ItemType() != nsIDocShellTreeItem::typeChrome) { nsCOMPtr docShell = do_QueryInterface(dsti); NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE); // don't move the caret for editable documents bool isEditable; docShell->GetEditable(&isEditable); if (isEditable) { return NS_OK; } RefPtr presShell = docShell->GetPresShell(); NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE); nsCOMPtr window = nsPIDOMWindowOuter::From(aWindow); if (RefPtr focusedElement = window->GetFocusedElement()) { MoveCaretToFocus(presShell, focusedElement); } } } return NS_OK; } void nsFocusManager::WindowRaised(mozIDOMWindowProxy* aWindow, uint64_t aActionId) { if (!aWindow) { return; } nsCOMPtr window = nsPIDOMWindowOuter::From(aWindow); BrowsingContext* bc = window->GetBrowsingContext(); if (MOZ_LOG_TEST(gFocusLog, LogLevel::Debug)) { LOGFOCUS(("Window %p Raised [Currently: %p %p] actionid: %" PRIu64, aWindow, mActiveWindow.get(), mFocusedWindow.get(), aActionId)); Document* doc = window->GetExtantDoc(); if (doc && doc->GetDocumentURI()) { LOGFOCUS((" Raised Window: %p %s", aWindow, doc->GetDocumentURI()->GetSpecOrDefault().get())); } if (mActiveWindow) { doc = mActiveWindow->GetExtantDoc(); if (doc && doc->GetDocumentURI()) { LOGFOCUS((" Active Window: %p %s", mActiveWindow.get(), doc->GetDocumentURI()->GetSpecOrDefault().get())); } } } if (XRE_IsParentProcess()) { if (mActiveWindow == window) { // The window is already active, so there is no need to focus anything, // but make sure that the right widget is focused. This is a special case // for Windows because when restoring a minimized window, a second // activation will occur and the top-level widget could be focused instead // of the child we want. We solve this by calling SetFocus to ensure that // what the focus manager thinks should be the current widget is actually // focused. EnsureCurrentWidgetFocused(CallerType::System); return; } // lower the existing window, if any. This shouldn't happen usually. if (nsCOMPtr activeWindow = mActiveWindow) { WindowLowered(activeWindow, aActionId); } } else if (bc->IsTop()) { BrowsingContext* active = GetActiveBrowsingContext(); if (active == bc && !mActiveBrowsingContextInContentSetFromOtherProcess) { // EnsureCurrentWidgetFocused() should not be necessary with // PuppetWidget. return; } if (active && active != bc) { if (active->IsInProcess()) { nsCOMPtr activeWindow = active->GetDOMWindow(); WindowLowered(activeWindow, aActionId); } // No else, because trying to lower other-process windows // from here can result in the BrowsingContext no longer // existing in the parent process by the time it deserializes // the IPC message. } } nsCOMPtr docShellAsItem = window->GetDocShell(); // If there's no docShellAsItem, this window must have been closed, // in that case there is no tree owner. if (!docShellAsItem) { return; } // set this as the active window if (XRE_IsParentProcess()) { mActiveWindow = window; } else if (bc->IsTop()) { SetActiveBrowsingContextInContent(bc, aActionId); } // ensure that the window is enabled and visible nsCOMPtr treeOwner; docShellAsItem->GetTreeOwner(getter_AddRefs(treeOwner)); nsCOMPtr baseWindow = do_QueryInterface(treeOwner); if (baseWindow) { bool isEnabled = true; if (NS_SUCCEEDED(baseWindow->GetEnabled(&isEnabled)) && !isEnabled) { return; } baseWindow->SetVisibility(true); } if (XRE_IsParentProcess()) { // Unsetting top-level focus upon lowering was inhibited to accommodate // ATOK, so we need to do it here. BrowserParent::UnsetTopLevelWebFocusAll(); ActivateOrDeactivate(window, true); } // retrieve the last focused element within the window that was raised nsCOMPtr currentWindow; RefPtr currentFocus = GetFocusedDescendant( window, eIncludeAllDescendants, getter_AddRefs(currentWindow)); NS_ASSERTION(currentWindow, "window raised with no window current"); if (!currentWindow) { return; } nsCOMPtr appWin(do_GetInterface(baseWindow)); // We use mFocusedWindow here is basically for the case that iframe navigate // from a.com to b.com for example, so it ends up being loaded in a different // process after Fission, but // currentWindow->GetBrowsingContext() == GetFocusedBrowsingContext() would // still be true because focused browsing context is synced, and we won't // fire a focus event while focusing if we use it as condition. Focus(currentWindow, currentFocus, 0, currentWindow != mFocusedWindow, false, appWin != nullptr, true, aActionId); } void nsFocusManager::WindowLowered(mozIDOMWindowProxy* aWindow, uint64_t aActionId) { if (!aWindow) { return; } nsCOMPtr window = nsPIDOMWindowOuter::From(aWindow); if (MOZ_LOG_TEST(gFocusLog, LogLevel::Debug)) { LOGFOCUS(("Window %p Lowered [Currently: %p %p]", aWindow, mActiveWindow.get(), mFocusedWindow.get())); Document* doc = window->GetExtantDoc(); if (doc && doc->GetDocumentURI()) { LOGFOCUS((" Lowered Window: %s", doc->GetDocumentURI()->GetSpecOrDefault().get())); } if (mActiveWindow) { doc = mActiveWindow->GetExtantDoc(); if (doc && doc->GetDocumentURI()) { LOGFOCUS((" Active Window: %s", doc->GetDocumentURI()->GetSpecOrDefault().get())); } } } if (XRE_IsParentProcess()) { if (mActiveWindow != window) { return; } } else { BrowsingContext* bc = window->GetBrowsingContext(); BrowsingContext* active = GetActiveBrowsingContext(); if (active != bc->Top()) { return; } } // clear the mouse capture as the active window has changed PresShell::ReleaseCapturingContent(); // In addition, reset the drag state to ensure that we are no longer in // drag-select mode. if (mFocusedWindow) { nsCOMPtr docShell = mFocusedWindow->GetDocShell(); if (docShell) { if (PresShell* presShell = docShell->GetPresShell()) { RefPtr frameSelection = presShell->FrameSelection(); frameSelection->SetDragState(false); } } } if (XRE_IsParentProcess()) { ActivateOrDeactivate(window, false); } // keep track of the window being lowered, so that attempts to raise the // window can be prevented until we return. Otherwise, focus can get into // an unusual state. mWindowBeingLowered = window; if (XRE_IsParentProcess()) { mActiveWindow = nullptr; } else { BrowsingContext* bc = window->GetBrowsingContext(); if (bc == bc->Top()) { SetActiveBrowsingContextInContent(nullptr, aActionId); } } if (mFocusedWindow) { Blur(nullptr, nullptr, true, true, false, aActionId); } mWindowBeingLowered = nullptr; } nsresult nsFocusManager::ContentRemoved(Document* aDocument, nsIContent* aContent) { NS_ENSURE_ARG(aDocument); NS_ENSURE_ARG(aContent); nsPIDOMWindowOuter* window = aDocument->GetWindow(); if (!window) { return NS_OK; } // if the content is currently focused in the window, or is an // shadow-including inclusive ancestor of the currently focused element, // reset the focus within that window. Element* content = window->GetFocusedElement(); if (!content) { return NS_OK; } if (!nsContentUtils::ContentIsHostIncludingDescendantOf(content, aContent)) { return NS_OK; } Element* newFocusedElement = [&]() -> Element* { if (auto* sr = ShadowRoot::FromNode(aContent)) { if (sr->IsUAWidget() && sr->Host()->IsHTMLElement(nsGkAtoms::input)) { return sr->Host(); } } return nullptr; }(); window->SetFocusedElement(newFocusedElement); // if this window is currently focused, clear the global focused // element as well, but don't fire any events. if (window->GetBrowsingContext() == GetFocusedBrowsingContext()) { mFocusedElement = newFocusedElement; } else if (Document* subdoc = aDocument->GetSubDocumentFor(content)) { // Check if the node that was focused is an iframe or similar by looking if // it has a subdocument. This would indicate that this focused iframe // and its descendants will be going away. We will need to move the focus // somewhere else, so just clear the focus in the toplevel window so that no // element is focused. // // The Fission case is handled in FlushAndCheckIfFocusable(). if (nsCOMPtr docShell = subdoc->GetDocShell()) { nsCOMPtr childWindow = docShell->GetWindow(); if (childWindow && IsSameOrAncestor(childWindow, GetFocusedBrowsingContext())) { if (XRE_IsParentProcess()) { nsCOMPtr activeWindow = mActiveWindow; ClearFocus(activeWindow); } else { BrowsingContext* active = GetActiveBrowsingContext(); if (active) { if (active->IsInProcess()) { nsCOMPtr activeWindow = active->GetDOMWindow(); ClearFocus(activeWindow); } else { mozilla::dom::ContentChild* contentChild = mozilla::dom::ContentChild::GetSingleton(); MOZ_ASSERT(contentChild); contentChild->SendClearFocus(active); } } // no else, because ClearFocus does nothing with nullptr } } } } // Notify the editor in case we removed its ancestor limiter. if (content->IsEditable()) { if (nsCOMPtr docShell = aDocument->GetDocShell()) { if (RefPtr htmlEditor = docShell->GetHTMLEditor()) { RefPtr selection = htmlEditor->GetSelection(); if (selection && selection->GetFrameSelection() && content == selection->GetFrameSelection()->GetAncestorLimiter()) { htmlEditor->FinalizeSelection(); } } } } if (!newFocusedElement) { NotifyFocusStateChange(content, newFocusedElement, 0, /* aGettingFocus = */ false, false); } else { // We should already have the right state, which is managed by the // widget. MOZ_ASSERT(newFocusedElement->State().HasState(ElementState::FOCUS)); } return NS_OK; } void nsFocusManager::WindowShown(mozIDOMWindowProxy* aWindow, bool aNeedsFocus) { if (!aWindow) { return; } nsCOMPtr window = nsPIDOMWindowOuter::From(aWindow); if (MOZ_LOG_TEST(gFocusLog, LogLevel::Debug)) { LOGFOCUS(("Window %p Shown [Currently: %p %p]", window.get(), mActiveWindow.get(), mFocusedWindow.get())); Document* doc = window->GetExtantDoc(); if (doc && doc->GetDocumentURI()) { LOGFOCUS(("Shown Window: %s", doc->GetDocumentURI()->GetSpecOrDefault().get())); } if (mFocusedWindow) { doc = mFocusedWindow->GetExtantDoc(); if (doc && doc->GetDocumentURI()) { LOGFOCUS((" Focused Window: %s", doc->GetDocumentURI()->GetSpecOrDefault().get())); } } } if (XRE_IsParentProcess()) { if (BrowsingContext* bc = window->GetBrowsingContext()) { if (bc->IsTop()) { bc->SetIsActiveBrowserWindow(bc->GetIsActiveBrowserWindow()); } } } if (XRE_IsParentProcess()) { if (mFocusedWindow != window) { return; } } else { BrowsingContext* bc = window->GetBrowsingContext(); if (!bc || mFocusedBrowsingContextInContent != bc) { return; } // Sync the window for a newly-created OOP iframe // Set actionId to zero to signify that it should be ignored. SetFocusedWindowInternal(window, 0, false); } if (aNeedsFocus) { nsCOMPtr currentWindow; RefPtr currentFocus = GetFocusedDescendant( window, eIncludeAllDescendants, getter_AddRefs(currentWindow)); if (currentWindow) { Focus(currentWindow, currentFocus, 0, true, false, false, true, GenerateFocusActionId()); } } else { // Sometimes, an element in a window can be focused before the window is // visible, which would mean that the widget may not be properly focused. // When the window becomes visible, make sure the right widget is focused. EnsureCurrentWidgetFocused(CallerType::System); } } void nsFocusManager::WindowHidden(mozIDOMWindowProxy* aWindow, uint64_t aActionId) { // if there is no window or it is not the same or an ancestor of the // currently focused window, just return, as the current focus will not // be affected. if (!aWindow) { return; } nsCOMPtr window = nsPIDOMWindowOuter::From(aWindow); if (MOZ_LOG_TEST(gFocusLog, LogLevel::Debug)) { LOGFOCUS(("Window %p Hidden [Currently: %p %p] actionid: %" PRIu64, window.get(), mActiveWindow.get(), mFocusedWindow.get(), aActionId)); nsAutoCString spec; Document* doc = window->GetExtantDoc(); if (doc && doc->GetDocumentURI()) { LOGFOCUS((" Hide Window: %s", doc->GetDocumentURI()->GetSpecOrDefault().get())); } if (mFocusedWindow) { doc = mFocusedWindow->GetExtantDoc(); if (doc && doc->GetDocumentURI()) { LOGFOCUS((" Focused Window: %s", doc->GetDocumentURI()->GetSpecOrDefault().get())); } } if (mActiveWindow) { doc = mActiveWindow->GetExtantDoc(); if (doc && doc->GetDocumentURI()) { LOGFOCUS((" Active Window: %s", doc->GetDocumentURI()->GetSpecOrDefault().get())); } } } if (!IsSameOrAncestor(window, mFocusedWindow)) { return; } // at this point, we know that the window being hidden is either the focused // window, or an ancestor of the focused window. Either way, the focus is no // longer valid, so it needs to be updated. const RefPtr oldFocusedElement = std::move(mFocusedElement); nsCOMPtr focusedDocShell = mFocusedWindow->GetDocShell(); if (!focusedDocShell) { return; } const RefPtr presShell = focusedDocShell->GetPresShell(); if (oldFocusedElement && oldFocusedElement->IsInComposedDoc()) { NotifyFocusStateChange(oldFocusedElement, nullptr, 0, false, false); window->UpdateCommands(u"focus"_ns, nullptr, 0); if (presShell) { RefPtr composedDoc = oldFocusedElement->GetComposedDoc(); SendFocusOrBlurEvent(eBlur, presShell, composedDoc, oldFocusedElement, false); } } const RefPtr focusedPresContext = presShell ? presShell->GetPresContext() : nullptr; IMEStateManager::OnChangeFocus(focusedPresContext, nullptr, GetFocusMoveActionCause(0)); if (presShell) { SetCaretVisible(presShell, false, nullptr); } // If a window is being "hidden" because its BrowsingContext is changing // remoteness, we don't want to handle docshell destruction by moving focus. // Instead, the focused browsing context should stay the way it is (so that // the newly "shown" window in the other process knows to take focus) and // we should just null out the process-local field. nsCOMPtr docShellBeingHidden = window->GetDocShell(); // Check if we're currently hiding a non-remote nsDocShell due to its // BrowsingContext navigating to become remote. Normally, when a focused // subframe is hidden, focus is moved to the frame element, but focus should // stay with the BrowsingContext when performing a process switch. We don't // need to consider process switches where the hiding docshell is already // remote (ie. GetEmbedderElement is nullptr), as shifting remoteness to the // frame element is handled elsewhere. if (docShellBeingHidden && nsDocShell::Cast(docShellBeingHidden)->WillChangeProcess() && docShellBeingHidden->GetBrowsingContext()->GetEmbedderElement()) { if (mFocusedWindow != window) { // The window being hidden is an ancestor of the focused window. #ifdef DEBUG BrowsingContext* ancestor = window->GetBrowsingContext(); BrowsingContext* bc = mFocusedWindow->GetBrowsingContext(); for (;;) { if (!bc) { MOZ_ASSERT(false, "Should have found ancestor"); } bc = bc->GetParent(); if (ancestor == bc) { break; } } #endif // This call adjusts the focused browsing context and window. // The latter gets nulled out immediately below. SetFocusedWindowInternal(window, aActionId); } mFocusedWindow = nullptr; window->SetFocusedElement(nullptr); return; } // if the docshell being hidden is being destroyed, then we want to move // focus somewhere else. Call ClearFocus on the toplevel window, which // will have the effect of clearing the focus and moving the focused window // to the toplevel window. But if the window isn't being destroyed, we are // likely just loading a new document in it, so we want to maintain the // focused window so that the new document gets properly focused. bool beingDestroyed = !docShellBeingHidden; if (docShellBeingHidden) { docShellBeingHidden->IsBeingDestroyed(&beingDestroyed); } if (beingDestroyed) { // There is usually no need to do anything if a toplevel window is going // away, as we assume that WindowLowered will be called. However, this may // not happen if nsIAppStartup::eForceQuit is used to quit, and can cause // a leak. So if the active window is being destroyed, call WindowLowered // directly. if (XRE_IsParentProcess()) { nsCOMPtr activeWindow = mActiveWindow; if (activeWindow == mFocusedWindow || activeWindow == window) { WindowLowered(activeWindow, aActionId); } else { ClearFocus(activeWindow); } } else { BrowsingContext* active = GetActiveBrowsingContext(); if (active) { if (nsCOMPtr activeWindow = active->GetDOMWindow()) { if ((mFocusedWindow && mFocusedWindow->GetBrowsingContext() == active) || (window->GetBrowsingContext() == active)) { WindowLowered(activeWindow, aActionId); } else { ClearFocus(activeWindow); } } // else do nothing when an out-of-process iframe is torn down } } return; } if (!XRE_IsParentProcess() && mActiveBrowsingContextInContent == docShellBeingHidden->GetBrowsingContext() && mActiveBrowsingContextInContent->GetIsInBFCache()) { SetActiveBrowsingContextInContent(nullptr, aActionId); } // if the window being hidden is an ancestor of the focused window, adjust // the focused window so that it points to the one being hidden. This // ensures that the focused window isn't in a chain of frames that doesn't // exist any more. if (window != mFocusedWindow) { nsCOMPtr dsti = mFocusedWindow ? mFocusedWindow->GetDocShell() : nullptr; if (dsti) { nsCOMPtr parentDsti; dsti->GetInProcessParent(getter_AddRefs(parentDsti)); if (parentDsti) { if (nsCOMPtr parentWindow = parentDsti->GetWindow()) { parentWindow->SetFocusedElement(nullptr); } } } SetFocusedWindowInternal(window, aActionId); } } void nsFocusManager::FireDelayedEvents(Document* aDocument) { MOZ_ASSERT(aDocument); // fire any delayed focus and blur events in the same order that they were // added for (uint32_t i = 0; i < mDelayedBlurFocusEvents.Length(); i++) { if (mDelayedBlurFocusEvents[i].mDocument == aDocument) { if (!aDocument->GetInnerWindow() || !aDocument->GetInnerWindow()->IsCurrentInnerWindow()) { // If the document was navigated away from or is defunct, don't bother // firing events on it. Note the symmetry between this condition and // the similar one in Document.cpp:FireOrClearDelayedEvents. mDelayedBlurFocusEvents.RemoveElementAt(i); --i; } else if (!aDocument->EventHandlingSuppressed()) { EventMessage message = mDelayedBlurFocusEvents[i].mEventMessage; nsCOMPtr target = mDelayedBlurFocusEvents[i].mTarget; RefPtr presShell = mDelayedBlurFocusEvents[i].mPresShell; nsCOMPtr relatedTarget = mDelayedBlurFocusEvents[i].mRelatedTarget; mDelayedBlurFocusEvents.RemoveElementAt(i); FireFocusOrBlurEvent(message, presShell, target, false, false, relatedTarget); --i; } } } } void nsFocusManager::WasNuked(nsPIDOMWindowOuter* aWindow) { MOZ_ASSERT(aWindow, "Expected non-null window."); MOZ_ASSERT(aWindow != mActiveWindow, "How come we're nuking a window that's still active?"); if (aWindow == mFocusedWindow) { mFocusedWindow = nullptr; SetFocusedBrowsingContext(nullptr, GenerateFocusActionId()); mFocusedElement = nullptr; } } nsFocusManager::BlurredElementInfo::BlurredElementInfo(Element& aElement) : mElement(aElement) {} nsFocusManager::BlurredElementInfo::~BlurredElementInfo() = default; // https://drafts.csswg.org/selectors-4/#the-focus-visible-pseudo static bool ShouldMatchFocusVisible(nsPIDOMWindowOuter* aWindow, const Element& aElement, int32_t aFocusFlags) { // If we were explicitly requested to show the ring, do it. if (aFocusFlags & nsIFocusManager::FLAG_SHOWRING) { return true; } if (aFocusFlags & nsIFocusManager::FLAG_NOSHOWRING) { return false; } if (aWindow->ShouldShowFocusRing()) { // The window decision also trumps any other heuristic. return true; } // Any element which supports keyboard input (such as an input element, or any // other element which may trigger a virtual keyboard to be shown on focus if // a physical keyboard is not present) should always match :focus-visible when // focused. { if (aElement.IsHTMLElement(nsGkAtoms::textarea) || aElement.IsEditable()) { return true; } if (auto* input = HTMLInputElement::FromNode(aElement)) { if (input->IsSingleLineTextControl()) { return true; } } } switch (nsFocusManager::GetFocusMoveActionCause(aFocusFlags)) { case InputContextAction::CAUSE_KEY: // If the user interacts with the page via the keyboard, the currently // focused element should match :focus-visible (i.e. keyboard usage may // change whether this pseudo-class matches even if it doesn't affect // :focus). return true; case InputContextAction::CAUSE_UNKNOWN: // We render outlines if the last "known" focus method was by key or there // was no previous known focus method, otherwise we don't. return aWindow->UnknownFocusMethodShouldShowOutline(); case InputContextAction::CAUSE_MOUSE: case InputContextAction::CAUSE_TOUCH: case InputContextAction::CAUSE_LONGPRESS: // If the user interacts with the page via a pointing device, such that // the focus is moved to a new element which does not support user input, // the newly focused element should not match :focus-visible. return false; case InputContextAction::CAUSE_UNKNOWN_CHROME: case InputContextAction::CAUSE_UNKNOWN_DURING_KEYBOARD_INPUT: case InputContextAction::CAUSE_UNKNOWN_DURING_NON_KEYBOARD_INPUT: // TODO(emilio): We could return some of these though, looking at // UserActivation. We may want to suppress focus rings for unknown / // programatic focus if the user is interacting with the page but not // during keyboard input, or such. MOZ_ASSERT_UNREACHABLE( "These don't get returned by GetFocusMoveActionCause"); break; } return false; } /* static */ void nsFocusManager::NotifyFocusStateChange(Element* aElement, Element* aElementToFocus, int32_t aFlags, bool aGettingFocus, bool aShouldShowFocusRing) { MOZ_ASSERT_IF(aElementToFocus, !aGettingFocus); nsIContent* commonAncestor = nullptr; if (aElementToFocus) { commonAncestor = nsContentUtils::GetCommonFlattenedTreeAncestor( aElement, aElementToFocus); } if (aGettingFocus) { ElementState stateToAdd = ElementState::FOCUS; if (aShouldShowFocusRing) { stateToAdd |= ElementState::FOCUSRING; } aElement->AddStates(stateToAdd); for (nsIContent* host = aElement->GetContainingShadowHost(); host; host = host->GetContainingShadowHost()) { host->AsElement()->AddStates(ElementState::FOCUS); } } else { constexpr auto kStatesToRemove = ElementState::FOCUS | ElementState::FOCUSRING; aElement->RemoveStates(kStatesToRemove); for (nsIContent* host = aElement->GetContainingShadowHost(); host; host = host->GetContainingShadowHost()) { host->AsElement()->RemoveStates(kStatesToRemove); } } // Special case for and . // The other browsers cancel active state when they gets lost focus, but // does not do it for the other elements such as