diff options
Diffstat (limited to '')
-rw-r--r-- | dom/base/nsFocusManager.cpp | 5515 |
1 files changed, 5515 insertions, 0 deletions
diff --git a/dom/base/nsFocusManager.cpp b/dom/base/nsFocusManager.cpp new file mode 100644 index 0000000000..08a2641333 --- /dev/null +++ b/dom/base/nsFocusManager.cpp @@ -0,0 +1,5515 @@ +/* -*- 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 "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/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/Document.h" +#include "mozilla/dom/DocumentInlines.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/HTMLAreaElement.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 "mozilla/Try.h" +#include "mozilla/widget/IMEData.h" +#include <algorithm> + +#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<PresShell> mPresShell; + nsCOMPtr<Document> mDocument; + nsCOMPtr<EventTarget> mTarget; + EventMessage mEventMessage; + nsCOMPtr<EventTarget> 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<nsIDocumentObserver*>(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> 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<nsIObserverService> 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<nsIObserverService> 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<nsFocusManager> fm = static_cast<nsFocusManager*>(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<BrowsingContext> 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<nsFocusManager> 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(("<<SetFocusedWindow begin>>")); + + nsCOMPtr<nsPIDOMWindowOuter> windowToFocus = + nsPIDOMWindowOuter::From(aWindowToFocus); + NS_ENSURE_TRUE(windowToFocus, NS_ERROR_FAILURE); + + nsCOMPtr<Element> frameElement = windowToFocus->GetFrameElementInternal(); + Maybe<uint64_t> 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<nsPIDOMWindowOuter> childWindow = GetContentWindow(content)) + ClearFocus(windowToFocus); + } + } + + nsCOMPtr<nsPIDOMWindowOuter> rootWindow = windowToFocus->GetPrivateRoot(); + const uint64_t actionId = actionIdFromSetFocusInner.isSome() + ? actionIdFromSetFocusInner.value() + : sInstance->GenerateFocusActionId(); + if (rootWindow) { + RaiseWindow(rootWindow, aCallerType, actionId); + } + + LOGFOCUS(("<<SetFocusedWindow end actionid: %" PRIu64 ">>", actionId)); + + return NS_OK; +} + +NS_IMETHODIMP nsFocusManager::SetFocusedWindow( + mozIDOMWindowProxy* aWindowToFocus) { + return SetFocusedWindowWithCallerType(aWindowToFocus, CallerType::System); +} + +NS_IMETHODIMP +nsFocusManager::GetFocusedElement(Element** aFocusedElement) { + RefPtr<Element> 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(("<<SetFocus begin>>")); + + NS_ENSURE_ARG(aElement); + + SetFocusInner(aElement, aFlags, true, true); + + LOGFOCUS(("<<SetFocus end>>")); + + 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(("<<MoveFocus begin Type: %d Flags: %x>>", 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<nsPIDOMWindowOuter> 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<Document> doc = window->GetExtantDoc()) { + doc->FlushPendingNotifications(FlushType::EnsurePresShellInitAndFrames); + } + + bool noParentTraversal = aFlags & FLAG_NOPARENTFRAME; + nsCOMPtr<nsIContent> 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(("<<MoveFocus end>>")); + + return NS_OK; +} + +NS_IMETHODIMP +nsFocusManager::ClearFocus(mozIDOMWindowProxy* aWindow) { + LOGFOCUS(("<<ClearFocus begin>>")); + + // 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<nsPIDOMWindowOuter> window = nsPIDOMWindowOuter::From(aWindow); + + if (IsSameOrAncestor(window, GetFocusedBrowsingContext())) { + RefPtr<BrowsingContext> 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(("<<ClearFocus end>>")); + + 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<nsPIDOMWindowOuter> window = nsPIDOMWindowOuter::From(aWindow); + + nsCOMPtr<nsPIDOMWindowOuter> focusedWindow; + RefPtr<Element> 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<nsIWebNavigation> webnav = do_GetInterface(aWindow); + nsCOMPtr<nsIDocShellTreeItem> dsti = do_QueryInterface(webnav); + if (dsti) { + if (dsti->ItemType() != nsIDocShellTreeItem::typeChrome) { + nsCOMPtr<nsIDocShell> 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> presShell = docShell->GetPresShell(); + NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE); + + nsCOMPtr<nsPIDOMWindowOuter> window = nsPIDOMWindowOuter::From(aWindow); + if (RefPtr<Element> focusedElement = window->GetFocusedElement()) { + MoveCaretToFocus(presShell, focusedElement); + } + } + } + + return NS_OK; +} + +void nsFocusManager::WindowRaised(mozIDOMWindowProxy* aWindow, + uint64_t aActionId) { + if (!aWindow) { + return; + } + + nsCOMPtr<nsPIDOMWindowOuter> 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<nsPIDOMWindowOuter> 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<nsPIDOMWindowOuter> 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<nsIDocShellTreeItem> 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<nsIDocShellTreeOwner> treeOwner; + docShellAsItem->GetTreeOwner(getter_AddRefs(treeOwner)); + nsCOMPtr<nsIBaseWindow> 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<nsPIDOMWindowOuter> currentWindow; + RefPtr<Element> currentFocus = GetFocusedDescendant( + window, eIncludeAllDescendants, getter_AddRefs(currentWindow)); + + NS_ASSERTION(currentWindow, "window raised with no window current"); + if (!currentWindow) { + return; + } + + nsCOMPtr<nsIAppWindow> 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<nsPIDOMWindowOuter> 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<nsIDocShell> docShell = mFocusedWindow->GetDocShell(); + if (docShell) { + if (PresShell* presShell = docShell->GetPresShell()) { + RefPtr<nsFrameSelection> 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); + + RefPtr<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. + RefPtr<Element> previousFocusedElement = window->GetFocusedElement(); + if (!previousFocusedElement) { + return NS_OK; + } + + if (!nsContentUtils::ContentIsHostIncludingDescendantOf( + previousFocusedElement, aContent)) { + return NS_OK; + } + + RefPtr<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(previousFocusedElement)) { + // 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<nsIDocShell> docShell = subdoc->GetDocShell()) { + nsCOMPtr<nsPIDOMWindowOuter> childWindow = docShell->GetWindow(); + if (childWindow && + IsSameOrAncestor(childWindow, GetFocusedBrowsingContext())) { + if (XRE_IsParentProcess()) { + nsCOMPtr<nsPIDOMWindowOuter> activeWindow = mActiveWindow; + ClearFocus(activeWindow); + } else { + BrowsingContext* active = GetActiveBrowsingContext(); + if (active) { + if (active->IsInProcess()) { + nsCOMPtr<nsPIDOMWindowOuter> 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 (previousFocusedElement->IsEditable()) { + if (nsCOMPtr<nsIDocShell> docShell = aDocument->GetDocShell()) { + if (RefPtr<HTMLEditor> htmlEditor = docShell->GetHTMLEditor()) { + RefPtr<Selection> selection = htmlEditor->GetSelection(); + if (selection && selection->GetFrameSelection() && + previousFocusedElement == + selection->GetFrameSelection()->GetAncestorLimiter()) { + htmlEditor->FinalizeSelection(); + } + } + } + } + + if (!newFocusedElement) { + NotifyFocusStateChange(previousFocusedElement, newFocusedElement, 0, + /* aGettingFocus = */ false, false); + } else { + // We should already have the right state, which is managed by the <input> + // widget. + MOZ_ASSERT(newFocusedElement->State().HasState(ElementState::FOCUS)); + } + + // If we changed focused element and the element still has focus, let's + // notify IME of focus. Note that if new focus move has already occurred + // by running script, we should not let IMEStateManager of outdated focus + // change. + if (mFocusedElement == newFocusedElement && mFocusedWindow == window) { + RefPtr<nsPresContext> presContext(aDocument->GetPresContext()); + IMEStateManager::OnChangeFocus(presContext, newFocusedElement, + InputContextAction::Cause::CAUSE_UNKNOWN); + } + + return NS_OK; +} + +void nsFocusManager::WindowShown(mozIDOMWindowProxy* aWindow, + bool aNeedsFocus) { + if (!aWindow) { + return; + } + + nsCOMPtr<nsPIDOMWindowOuter> 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<nsPIDOMWindowOuter> currentWindow; + RefPtr<Element> 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<nsPIDOMWindowOuter> 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<Element> oldFocusedElement = std::move(mFocusedElement); + + nsCOMPtr<nsIDocShell> focusedDocShell = mFocusedWindow->GetDocShell(); + if (!focusedDocShell) { + return; + } + + const RefPtr<PresShell> presShell = focusedDocShell->GetPresShell(); + + if (oldFocusedElement && oldFocusedElement->IsInComposedDoc()) { + NotifyFocusStateChange(oldFocusedElement, nullptr, 0, false, false); + window->UpdateCommands(u"focus"_ns); + + if (presShell) { + RefPtr<Document> composedDoc = oldFocusedElement->GetComposedDoc(); + SendFocusOrBlurEvent(eBlur, presShell, composedDoc, oldFocusedElement, + false); + } + } + + const RefPtr<nsPresContext> 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<nsIDocShell> 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<nsPIDOMWindowOuter> activeWindow = mActiveWindow; + if (activeWindow == mFocusedWindow || activeWindow == window) { + WindowLowered(activeWindow, aActionId); + } else { + ClearFocus(activeWindow); + } + } else { + BrowsingContext* active = GetActiveBrowsingContext(); + if (active) { + if (nsCOMPtr<nsPIDOMWindowOuter> 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<nsIDocShellTreeItem> dsti = + mFocusedWindow ? mFocusedWindow->GetDocShell() : nullptr; + if (dsti) { + nsCOMPtr<nsIDocShellTreeItem> parentDsti; + dsti->GetInProcessParent(getter_AddRefs(parentDsti)); + if (parentDsti) { + if (nsCOMPtr<nsPIDOMWindowOuter> 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<EventTarget> target = mDelayedBlurFocusEvents[i].mTarget; + RefPtr<PresShell> presShell = mDelayedBlurFocusEvents[i].mPresShell; + nsCOMPtr<EventTarget> 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 <input type="checkbox"> and <input type="radio">. + // The other browsers cancel active state when they gets lost focus, but + // does not do it for the other elements such as <button> and <a href="...">. + // Additionally, they may be activated with <label>, but they will get focus + // at `click`, but activated at `mousedown`. Therefore, we need to cancel + // active state at moving focus. + if (RefPtr<nsPresContext> presContext = + aElement->GetPresContext(Element::PresContextFor::eForComposedDoc)) { + RefPtr<EventStateManager> esm = presContext->EventStateManager(); + auto* activeInputElement = + HTMLInputElement::FromNodeOrNull(esm->GetActiveContent()); + if (activeInputElement && + (activeInputElement->ControlType() == FormControlType::InputCheckbox || + activeInputElement->ControlType() == FormControlType::InputRadio) && + !activeInputElement->State().HasState(ElementState::FOCUS)) { + esm->SetContentState(nullptr, ElementState::ACTIVE); + } + } + + for (nsIContent* content = aElement; content && content != commonAncestor; + content = content->GetFlattenedTreeParent()) { + Element* element = Element::FromNode(content); + if (!element) { + continue; + } + + if (aGettingFocus) { + if (element->State().HasState(ElementState::FOCUS_WITHIN)) { + break; + } + element->AddStates(ElementState::FOCUS_WITHIN); + } else { + element->RemoveStates(ElementState::FOCUS_WITHIN); + } + } +} + +// static +void nsFocusManager::EnsureCurrentWidgetFocused(CallerType aCallerType) { + if (!mFocusedWindow || sTestMode) return; + + // get the main child widget for the focused window and ensure that the + // platform knows that this widget is focused. + nsCOMPtr<nsIDocShell> docShell = mFocusedWindow->GetDocShell(); + if (!docShell) { + return; + } + RefPtr<PresShell> presShell = docShell->GetPresShell(); + if (!presShell) { + return; + } + nsViewManager* vm = presShell->GetViewManager(); + if (!vm) { + return; + } + nsCOMPtr<nsIWidget> widget = vm->GetRootWidget(); + if (!widget) { + return; + } + widget->SetFocus(nsIWidget::Raise::No, aCallerType); +} + +void nsFocusManager::ActivateOrDeactivate(nsPIDOMWindowOuter* aWindow, + bool aActive) { + MOZ_ASSERT(XRE_IsParentProcess()); + if (!aWindow) { + return; + } + + if (BrowsingContext* bc = aWindow->GetBrowsingContext()) { + MOZ_ASSERT(bc->IsTop()); + + RefPtr<CanonicalBrowsingContext> chromeTop = + bc->Canonical()->TopCrossChromeBoundary(); + MOZ_ASSERT(bc == chromeTop); + + chromeTop->SetIsActiveBrowserWindow(aActive); + chromeTop->CallOnAllTopDescendants( + [aActive](CanonicalBrowsingContext* aBrowsingContext) { + aBrowsingContext->SetIsActiveBrowserWindow(aActive); + return CallState::Continue; + }, + /* aIncludeNestedBrowsers = */ true); + } + + if (aWindow->GetExtantDoc()) { + nsContentUtils::DispatchEventOnlyToChrome( + aWindow->GetExtantDoc(), + nsGlobalWindowInner::Cast(aWindow->GetCurrentInnerWindow()), + aActive ? u"activate"_ns : u"deactivate"_ns, CanBubble::eYes, + Cancelable::eYes, nullptr); + } +} + +// Retrieves innerWindowId of the window of the last focused element to +// log a warning to the website console. +void LogWarningFullscreenWindowRaise(Element* aElement) { + nsCOMPtr<nsFrameLoaderOwner> frameLoaderOwner(do_QueryInterface(aElement)); + NS_ENSURE_TRUE_VOID(frameLoaderOwner); + + RefPtr<nsFrameLoader> frameLoader = frameLoaderOwner->GetFrameLoader(); + NS_ENSURE_TRUE_VOID(frameLoaderOwner); + + RefPtr<BrowsingContext> browsingContext = frameLoader->GetBrowsingContext(); + NS_ENSURE_TRUE_VOID(browsingContext); + + WindowGlobalParent* windowGlobalParent = + browsingContext->Canonical()->GetCurrentWindowGlobal(); + NS_ENSURE_TRUE_VOID(windowGlobalParent); + + // Log to console + nsAutoString localizedMsg; + nsTArray<nsString> params; + nsresult rv = nsContentUtils::FormatLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "FullscreenExitWindowFocus", params, + localizedMsg); + + NS_ENSURE_SUCCESS_VOID(rv); + + Unused << nsContentUtils::ReportToConsoleByWindowID( + localizedMsg, nsIScriptError::warningFlag, "DOM"_ns, + windowGlobalParent->InnerWindowId(), + windowGlobalParent->GetDocumentURI()); +} + +// Ensure that when an embedded popup with a noautofocus attribute +// like a date picker is opened and focused, the parent page does not blur +static bool IsEmeddededInNoautofocusPopup(BrowsingContext& aBc) { + auto* embedder = aBc.GetEmbedderElement(); + if (!embedder) { + return false; + } + nsIFrame* f = embedder->GetPrimaryFrame(); + if (!f || !f->HasAnyStateBits(NS_FRAME_IN_POPUP)) { + return false; + } + + nsIFrame* menuPopup = + nsLayoutUtils::GetClosestFrameOfType(f, LayoutFrameType::MenuPopup); + MOZ_ASSERT(menuPopup, "NS_FRAME_IN_POPUP lied?"); + return static_cast<nsMenuPopupFrame*>(menuPopup) + ->PopupElement() + .GetXULBoolAttr(nsGkAtoms::noautofocus); +} + +Maybe<uint64_t> nsFocusManager::SetFocusInner(Element* aNewContent, + int32_t aFlags, + bool aFocusChanged, + bool aAdjustWidget) { + // if the element is not focusable, just return and leave the focus as is + RefPtr<Element> elementToFocus = + FlushAndCheckIfFocusable(aNewContent, aFlags); + if (!elementToFocus) { + return Nothing(); + } + + const RefPtr<BrowsingContext> focusedBrowsingContext = + GetFocusedBrowsingContext(); + + // check if the element to focus is a frame (iframe) containing a child + // document. Frames are never directly focused; instead focusing a frame + // means focus what is inside the frame. To do this, the descendant content + // within the frame is retrieved and that will be focused instead. + nsCOMPtr<nsPIDOMWindowOuter> newWindow; + nsCOMPtr<nsPIDOMWindowOuter> subWindow = GetContentWindow(elementToFocus); + if (subWindow) { + elementToFocus = GetFocusedDescendant(subWindow, eIncludeAllDescendants, + getter_AddRefs(newWindow)); + + // since a window is being refocused, clear aFocusChanged so that the + // caret position isn't updated. + aFocusChanged = false; + } + + // unless it was set above, retrieve the window for the element to focus + if (!newWindow) { + newWindow = GetCurrentWindow(elementToFocus); + } + + RefPtr<BrowsingContext> newBrowsingContext; + if (newWindow) { + newBrowsingContext = newWindow->GetBrowsingContext(); + } + + // if the element is already focused, just return. Note that this happens + // after the frame check above so that we compare the element that will be + // focused rather than the frame it is in. + if (!newWindow || (newBrowsingContext == GetFocusedBrowsingContext() && + elementToFocus == mFocusedElement)) { + return Nothing(); + } + + MOZ_ASSERT(newBrowsingContext); + + BrowsingContext* browsingContextToFocus = newBrowsingContext; + if (RefPtr<nsFrameLoaderOwner> flo = do_QueryObject(elementToFocus)) { + // Only look at pre-existing browsing contexts. If this function is + // called during reflow, calling GetBrowsingContext() could cause frame + // loader initialization at a time when it isn't safe. + if (BrowsingContext* bc = flo->GetExtantBrowsingContext()) { + // If focus is already in the subtree rooted at bc, return early + // to match the single-process focus semantics. Otherwise, we'd + // blur and immediately refocus whatever is focused. + BrowsingContext* walk = focusedBrowsingContext; + while (walk) { + if (walk == bc) { + return Nothing(); + } + walk = walk->GetParent(); + } + browsingContextToFocus = bc; + } + } + + // don't allow focus to be placed in docshells or descendants of docshells + // that are being destroyed. Also, ensure that the page hasn't been + // unloaded. The prevents content from being refocused during an unload event. + nsCOMPtr<nsIDocShell> newDocShell = newWindow->GetDocShell(); + nsCOMPtr<nsIDocShell> docShell = newDocShell; + while (docShell) { + bool inUnload; + docShell->GetIsInUnload(&inUnload); + if (inUnload) { + return Nothing(); + } + + bool beingDestroyed; + docShell->IsBeingDestroyed(&beingDestroyed); + if (beingDestroyed) { + return Nothing(); + } + + BrowsingContext* bc = docShell->GetBrowsingContext(); + + nsCOMPtr<nsIDocShellTreeItem> parentDsti; + docShell->GetInProcessParent(getter_AddRefs(parentDsti)); + docShell = do_QueryInterface(parentDsti); + if (!docShell && !XRE_IsParentProcess()) { + // We don't have an in-process parent, but let's see if we have + // an in-process ancestor or if an out-of-process ancestor + // is discarded. + do { + bc = bc->GetParent(); + if (bc && bc->IsDiscarded()) { + return Nothing(); + } + } while (bc && !bc->IsInProcess()); + if (bc) { + docShell = bc->GetDocShell(); + } else { + docShell = nullptr; + } + } + } + + bool focusMovesToDifferentBC = + (focusedBrowsingContext != browsingContextToFocus); + + if (focusedBrowsingContext && focusMovesToDifferentBC && + nsContentUtils::IsHandlingKeyBoardEvent() && + !nsContentUtils::LegacyIsCallerChromeOrNativeCode()) { + MOZ_ASSERT(browsingContextToFocus, + "BrowsingContext to focus should be non-null."); + + nsIPrincipal* focusedPrincipal = nullptr; + nsIPrincipal* newPrincipal = nullptr; + + if (XRE_IsParentProcess()) { + if (WindowGlobalParent* focusedWindowGlobalParent = + focusedBrowsingContext->Canonical()->GetCurrentWindowGlobal()) { + focusedPrincipal = focusedWindowGlobalParent->DocumentPrincipal(); + } + + if (WindowGlobalParent* newWindowGlobalParent = + browsingContextToFocus->Canonical()->GetCurrentWindowGlobal()) { + newPrincipal = newWindowGlobalParent->DocumentPrincipal(); + } + } else if (focusedBrowsingContext->IsInProcess() && + browsingContextToFocus->IsInProcess()) { + nsCOMPtr<nsIScriptObjectPrincipal> focused = + do_QueryInterface(focusedBrowsingContext->GetDOMWindow()); + nsCOMPtr<nsIScriptObjectPrincipal> newFocus = + do_QueryInterface(browsingContextToFocus->GetDOMWindow()); + MOZ_ASSERT(focused && newFocus, + "BrowsingContext should always have a window here."); + focusedPrincipal = focused->GetPrincipal(); + newPrincipal = newFocus->GetPrincipal(); + } + + if (!focusedPrincipal || !newPrincipal) { + return Nothing(); + } + + if (!focusedPrincipal->Subsumes(newPrincipal)) { + NS_WARNING("Not allowed to focus the new window!"); + return Nothing(); + } + } + + // to check if the new element is in the active window, compare the + // new root docshell for the new element with the active window's docshell. + RefPtr<BrowsingContext> newRootBrowsingContext = nullptr; + bool isElementInActiveWindow = false; + if (XRE_IsParentProcess()) { + nsCOMPtr<nsPIDOMWindowOuter> newRootWindow = nullptr; + nsCOMPtr<nsIDocShellTreeItem> dsti = newWindow->GetDocShell(); + if (dsti) { + nsCOMPtr<nsIDocShellTreeItem> root; + dsti->GetInProcessRootTreeItem(getter_AddRefs(root)); + newRootWindow = root ? root->GetWindow() : nullptr; + + isElementInActiveWindow = + (mActiveWindow && newRootWindow == mActiveWindow); + } + if (newRootWindow) { + newRootBrowsingContext = newRootWindow->GetBrowsingContext(); + } + } else { + // XXX This is wrong for `<iframe mozbrowser>` and for XUL + // `<browser remote="true">`. See: + // https://searchfox.org/mozilla-central/rev/8a63fc190b39ed6951abb4aef4a56487a43962bc/dom/base/nsFrameLoader.cpp#229-232 + newRootBrowsingContext = newBrowsingContext->Top(); + // to check if the new element is in the active window, compare the + // new root docshell for the new element with the active window's docshell. + isElementInActiveWindow = + (GetActiveBrowsingContext() == newRootBrowsingContext); + } + + // Exit fullscreen if a website focuses another window + if (StaticPrefs::full_screen_api_exit_on_windowRaise() && + !isElementInActiveWindow && (aFlags & FLAG_RAISE)) { + if (XRE_IsParentProcess()) { + if (Document* doc = mActiveWindow ? mActiveWindow->GetDoc() : nullptr) { + Document::ClearPendingFullscreenRequests(doc); + if (doc->GetFullscreenElement()) { + LogWarningFullscreenWindowRaise(mFocusedElement); + Document::AsyncExitFullscreen(doc); + } + } + } else { + BrowsingContext* activeBrowsingContext = GetActiveBrowsingContext(); + if (activeBrowsingContext) { + nsIDocShell* shell = activeBrowsingContext->GetDocShell(); + if (shell) { + if (Document* doc = shell->GetDocument()) { + Document::ClearPendingFullscreenRequests(doc); + if (doc->GetFullscreenElement()) { + Document::AsyncExitFullscreen(doc); + } + } + } else { + mozilla::dom::ContentChild* contentChild = + mozilla::dom::ContentChild::GetSingleton(); + MOZ_ASSERT(contentChild); + contentChild->SendMaybeExitFullscreen(activeBrowsingContext); + } + } + } + } + + // if the FLAG_NOSWITCHFRAME flag is used, only allow the focus to be + // shifted away from the current element if the new shell to focus is + // the same or an ancestor shell of the currently focused shell. + bool allowFrameSwitch = !(aFlags & FLAG_NOSWITCHFRAME) || + IsSameOrAncestor(newWindow, focusedBrowsingContext); + + // if the element is in the active window, frame switching is allowed and + // the content is in a visible window, fire blur and focus events. + bool sendFocusEvent = + isElementInActiveWindow && allowFrameSwitch && IsWindowVisible(newWindow); + + // Don't allow to steal the focus from chrome nodes if the caller cannot + // access them. + if (sendFocusEvent && mFocusedElement && + mFocusedElement->OwnerDoc() != aNewContent->OwnerDoc() && + mFocusedElement->NodePrincipal()->IsSystemPrincipal() && + !nsContentUtils::LegacyIsCallerNativeCode() && + !nsContentUtils::CanCallerAccess(mFocusedElement)) { + sendFocusEvent = false; + } + + LOGCONTENT("Shift Focus: %s", elementToFocus.get()); + LOGFOCUS((" Flags: %x Current Window: %p New Window: %p Current Element: %p", + aFlags, mFocusedWindow.get(), newWindow.get(), + mFocusedElement.get())); + const uint64_t actionId = GenerateFocusActionId(); + LOGFOCUS( + (" In Active Window: %d Moves to different BrowsingContext: %d " + "SendFocus: %d actionid: %" PRIu64, + isElementInActiveWindow, focusMovesToDifferentBC, sendFocusEvent, + actionId)); + + if (sendFocusEvent) { + Maybe<BlurredElementInfo> blurredInfo; + if (mFocusedElement) { + blurredInfo.emplace(*mFocusedElement); + } + // return if blurring fails or the focus changes during the blur + if (focusedBrowsingContext) { + // find the common ancestor of the currently focused window and the new + // window. The ancestor will need to have its currently focused node + // cleared once the document has been blurred. Otherwise, we'll be in a + // state where a document is blurred yet the chain of windows above it + // still points to that document. + // For instance, in the following frame tree: + // A + // B C + // D + // D is focused and we want to focus C. Once D has been blurred, we need + // to clear out the focus in A, otherwise A would still maintain that B + // was focused, and B that D was focused. + RefPtr<BrowsingContext> commonAncestor = + focusMovesToDifferentBC + ? GetCommonAncestor(newWindow, focusedBrowsingContext) + : nullptr; + + const bool needToClearFocusedElement = [&] { + if (focusedBrowsingContext->IsChrome()) { + // Always reset focused element if focus is currently in chrome + // window, unless we're moving focus to a popup. + return !IsEmeddededInNoautofocusPopup(*browsingContextToFocus); + } + if (focusedBrowsingContext->Top() != browsingContextToFocus->Top()) { + // Only reset focused element if focus moves within the same top-level + // content window. + return false; + } + // XXX for the case that we try to focus an + // already-focused-remote-frame, we would still send blur and focus + // IPC to it, but they will not generate blur or focus event, we don't + // want to reset activeElement on the remote frame. + return focusMovesToDifferentBC || focusedBrowsingContext->IsInProcess(); + }(); + + const bool remainActive = + focusMovesToDifferentBC && + IsEmeddededInNoautofocusPopup(*browsingContextToFocus); + + // TODO: MOZ_KnownLive is required due to bug 1770680 + if (!Blur(MOZ_KnownLive(needToClearFocusedElement + ? focusedBrowsingContext.get() + : nullptr), + commonAncestor, focusMovesToDifferentBC, aAdjustWidget, + remainActive, actionId, elementToFocus)) { + return Some(actionId); + } + } + + Focus(newWindow, elementToFocus, aFlags, focusMovesToDifferentBC, + aFocusChanged, false, aAdjustWidget, actionId, blurredInfo); + } else { + // otherwise, for inactive windows and when the caller cannot steal the + // focus, update the node in the window, and raise the window if desired. + if (allowFrameSwitch) { + AdjustWindowFocus(newBrowsingContext, true, IsWindowVisible(newWindow), + actionId); + } + + // set the focus node and method as needed + uint32_t focusMethod = + aFocusChanged ? aFlags & METHODANDRING_MASK + : newWindow->GetFocusMethod() | + (aFlags & (FLAG_SHOWRING | FLAG_NOSHOWRING)); + newWindow->SetFocusedElement(elementToFocus, focusMethod); + if (aFocusChanged) { + if (nsCOMPtr<nsIDocShell> docShell = newWindow->GetDocShell()) { + RefPtr<PresShell> presShell = docShell->GetPresShell(); + if (presShell && presShell->DidInitialize()) { + ScrollIntoView(presShell, elementToFocus, aFlags); + } + } + } + + // update the commands even when inactive so that the attributes for that + // window are up to date. + if (allowFrameSwitch) { + newWindow->UpdateCommands(u"focus"_ns); + } + + if (aFlags & FLAG_RAISE) { + if (newRootBrowsingContext) { + if (XRE_IsParentProcess() || newRootBrowsingContext->IsInProcess()) { + nsCOMPtr<nsPIDOMWindowOuter> outerWindow = + newRootBrowsingContext->GetDOMWindow(); + RaiseWindow(outerWindow, + aFlags & FLAG_NONSYSTEMCALLER ? CallerType::NonSystem + : CallerType::System, + actionId); + } else { + mozilla::dom::ContentChild* contentChild = + mozilla::dom::ContentChild::GetSingleton(); + MOZ_ASSERT(contentChild); + contentChild->SendRaiseWindow(newRootBrowsingContext, + aFlags & FLAG_NONSYSTEMCALLER + ? CallerType::NonSystem + : CallerType::System, + actionId); + } + } + } + } + return Some(actionId); +} + +static BrowsingContext* GetParentIgnoreChromeBoundary(BrowsingContext* aBC) { + // Chrome BrowsingContexts are only available in the parent process, so if + // we're in a content process, we only worry about the context tree. + if (XRE_IsParentProcess()) { + return aBC->Canonical()->GetParentCrossChromeBoundary(); + } + return aBC->GetParent(); +} + +bool nsFocusManager::IsSameOrAncestor(BrowsingContext* aPossibleAncestor, + BrowsingContext* aContext) const { + if (!aPossibleAncestor) { + return false; + } + + for (BrowsingContext* bc = aContext; bc; + bc = GetParentIgnoreChromeBoundary(bc)) { + if (bc == aPossibleAncestor) { + return true; + } + } + + return false; +} + +bool nsFocusManager::IsSameOrAncestor(nsPIDOMWindowOuter* aPossibleAncestor, + nsPIDOMWindowOuter* aWindow) const { + if (aWindow && aPossibleAncestor) { + return IsSameOrAncestor(aPossibleAncestor->GetBrowsingContext(), + aWindow->GetBrowsingContext()); + } + return false; +} + +bool nsFocusManager::IsSameOrAncestor(nsPIDOMWindowOuter* aPossibleAncestor, + BrowsingContext* aContext) const { + if (aPossibleAncestor) { + return IsSameOrAncestor(aPossibleAncestor->GetBrowsingContext(), aContext); + } + return false; +} + +bool nsFocusManager::IsSameOrAncestor(BrowsingContext* aPossibleAncestor, + nsPIDOMWindowOuter* aWindow) const { + if (aWindow) { + return IsSameOrAncestor(aPossibleAncestor, aWindow->GetBrowsingContext()); + } + return false; +} + +mozilla::dom::BrowsingContext* nsFocusManager::GetCommonAncestor( + nsPIDOMWindowOuter* aWindow, mozilla::dom::BrowsingContext* aContext) { + NS_ENSURE_TRUE(aWindow && aContext, nullptr); + + if (XRE_IsParentProcess()) { + nsCOMPtr<nsIDocShellTreeItem> dsti1 = aWindow->GetDocShell(); + NS_ENSURE_TRUE(dsti1, nullptr); + + nsCOMPtr<nsIDocShellTreeItem> dsti2 = aContext->GetDocShell(); + NS_ENSURE_TRUE(dsti2, nullptr); + + AutoTArray<nsIDocShellTreeItem*, 30> parents1, parents2; + do { + parents1.AppendElement(dsti1); + nsCOMPtr<nsIDocShellTreeItem> parentDsti1; + dsti1->GetInProcessParent(getter_AddRefs(parentDsti1)); + dsti1.swap(parentDsti1); + } while (dsti1); + do { + parents2.AppendElement(dsti2); + nsCOMPtr<nsIDocShellTreeItem> parentDsti2; + dsti2->GetInProcessParent(getter_AddRefs(parentDsti2)); + dsti2.swap(parentDsti2); + } while (dsti2); + + uint32_t pos1 = parents1.Length(); + uint32_t pos2 = parents2.Length(); + nsIDocShellTreeItem* parent = nullptr; + uint32_t len; + for (len = std::min(pos1, pos2); len > 0; --len) { + nsIDocShellTreeItem* child1 = parents1.ElementAt(--pos1); + nsIDocShellTreeItem* child2 = parents2.ElementAt(--pos2); + if (child1 != child2) { + break; + } + parent = child1; + } + + return parent ? parent->GetBrowsingContext() : nullptr; + } + + BrowsingContext* bc1 = aWindow->GetBrowsingContext(); + NS_ENSURE_TRUE(bc1, nullptr); + + BrowsingContext* bc2 = aContext; + + AutoTArray<BrowsingContext*, 30> parents1, parents2; + do { + parents1.AppendElement(bc1); + bc1 = bc1->GetParent(); + } while (bc1); + do { + parents2.AppendElement(bc2); + bc2 = bc2->GetParent(); + } while (bc2); + + uint32_t pos1 = parents1.Length(); + uint32_t pos2 = parents2.Length(); + BrowsingContext* parent = nullptr; + uint32_t len; + for (len = std::min(pos1, pos2); len > 0; --len) { + BrowsingContext* child1 = parents1.ElementAt(--pos1); + BrowsingContext* child2 = parents2.ElementAt(--pos2); + if (child1 != child2) { + break; + } + parent = child1; + } + + return parent; +} + +bool nsFocusManager::AdjustInProcessWindowFocus( + BrowsingContext* aBrowsingContext, bool aCheckPermission, bool aIsVisible, + uint64_t aActionId) { + if (ActionIdComparableAndLower(aActionId, + mActionIdForFocusedBrowsingContextInContent)) { + LOGFOCUS( + ("Ignored an attempt to adjust an in-process BrowsingContext [%p] as " + "focused from another process due to stale action id %" PRIu64 ".", + aBrowsingContext, aActionId)); + return false; + } + + BrowsingContext* bc = aBrowsingContext; + bool needToNotifyOtherProcess = false; + while (bc) { + // get the containing <iframe> or equivalent element so that it can be + // focused below. + nsCOMPtr<Element> frameElement = bc->GetEmbedderElement(); + BrowsingContext* parent = bc->GetParent(); + if (!parent && XRE_IsParentProcess()) { + CanonicalBrowsingContext* canonical = bc->Canonical(); + RefPtr<WindowGlobalParent> embedder = + canonical->GetEmbedderWindowGlobal(); + if (embedder) { + parent = embedder->BrowsingContext(); + } + } + bc = parent; + if (!bc) { + break; + } + if (!frameElement && XRE_IsContentProcess()) { + needToNotifyOtherProcess = true; + continue; + } + + nsCOMPtr<nsPIDOMWindowOuter> window = bc->GetDOMWindow(); + MOZ_ASSERT(window); + // if the parent window is visible but the original window was not, then we + // have likely moved up and out from a hidden tab to the browser window, or + // a similar such arrangement. Stop adjusting the current nodes. + if (IsWindowVisible(window) != aIsVisible) { + break; + } + + // When aCheckPermission is true, we should check whether the caller can + // access the window or not. If it cannot access, we should stop the + // adjusting. + if (aCheckPermission && !nsContentUtils::LegacyIsCallerNativeCode() && + !nsContentUtils::CanCallerAccess(window->GetCurrentInnerWindow())) { + break; + } + + if (frameElement != window->GetFocusedElement()) { + window->SetFocusedElement(frameElement); + + RefPtr<nsFrameLoaderOwner> loaderOwner = do_QueryObject(frameElement); + MOZ_ASSERT(loaderOwner); + RefPtr<nsFrameLoader> loader = loaderOwner->GetFrameLoader(); + if (loader && loader->IsRemoteFrame() && + GetFocusedBrowsingContext() == bc) { + Blur(nullptr, nullptr, true, true, false, aActionId); + } + } + } + return needToNotifyOtherProcess; +} + +void nsFocusManager::AdjustWindowFocus(BrowsingContext* aBrowsingContext, + bool aCheckPermission, bool aIsVisible, + uint64_t aActionId) { + if (AdjustInProcessWindowFocus(aBrowsingContext, aCheckPermission, aIsVisible, + aActionId)) { + // Some ancestors of aBrowsingContext isn't in this process, so notify other + // processes to adjust their focused element. + mozilla::dom::ContentChild* contentChild = + mozilla::dom::ContentChild::GetSingleton(); + MOZ_ASSERT(contentChild); + contentChild->SendAdjustWindowFocus(aBrowsingContext, aIsVisible, + aActionId); + } +} + +bool nsFocusManager::IsWindowVisible(nsPIDOMWindowOuter* aWindow) { + if (!aWindow || aWindow->IsFrozen()) { + return false; + } + + // Check if the inner window is frozen as well. This can happen when a focus + // change occurs while restoring a previous page. + nsPIDOMWindowInner* innerWindow = aWindow->GetCurrentInnerWindow(); + if (!innerWindow || innerWindow->IsFrozen()) { + return false; + } + + nsCOMPtr<nsIDocShell> docShell = aWindow->GetDocShell(); + nsCOMPtr<nsIBaseWindow> baseWin(do_QueryInterface(docShell)); + if (!baseWin) { + return false; + } + + bool visible = false; + baseWin->GetVisibility(&visible); + return visible; +} + +bool nsFocusManager::IsNonFocusableRoot(nsIContent* aContent) { + MOZ_ASSERT(aContent, "aContent must not be NULL"); + MOZ_ASSERT(aContent->IsInComposedDoc(), "aContent must be in a document"); + + // If the uncomposed document of aContent is in designMode, the root element + // is not focusable. + // NOTE: Most elements whose uncomposed document is in design mode are not + // focusable, just the document is focusable. However, if it's in a + // shadow tree, it may be focus able even if the shadow host is in + // design mode. + // Also, if aContent is not editable and it's not in designMode, it's not + // focusable. + // And in userfocusignored context nothing is focusable. + Document* doc = aContent->GetComposedDoc(); + NS_ASSERTION(doc, "aContent must have current document"); + return aContent == doc->GetRootElement() && + (aContent->IsInDesignMode() || !aContent->IsEditable()); +} + +Element* nsFocusManager::FlushAndCheckIfFocusable(Element* aElement, + uint32_t aFlags) { + if (!aElement) { + return nullptr; + } + + nsCOMPtr<Document> doc = aElement->GetComposedDoc(); + // can't focus elements that are not in documents + if (!doc) { + LOGCONTENT("Cannot focus %s because content not in document", aElement) + return nullptr; + } + + // Make sure that our frames are up to date while ensuring the presshell is + // also initialized in case we come from a script calling focus() early. + mEventHandlingNeedsFlush = false; + doc->FlushPendingNotifications(FlushType::EnsurePresShellInitAndFrames); + + PresShell* presShell = doc->GetPresShell(); + if (!presShell) { + return nullptr; + } + + // If this is an iframe that doesn't have an in-process subdocument, it is + // either an OOP iframe or an in-process iframe without lazy about:blank + // creation having taken place. In the OOP case, iframe is always focusable. + // In the in-process case, create the initial about:blank for in-process + // BrowsingContexts in order to have the `GetSubDocumentFor` call after this + // block return something. + // + // TODO(emilio): This block can probably go after bug 543435 lands. + if (RefPtr<nsFrameLoaderOwner> flo = do_QueryObject(aElement)) { + if (!aElement->IsXULElement()) { + // Only look at pre-existing browsing contexts. If this function is + // called during reflow, calling GetBrowsingContext() could cause frame + // loader initialization at a time when it isn't safe. + if (BrowsingContext* bc = flo->GetExtantBrowsingContext()) { + // This call may create a contentViewer-created about:blank. + // That's intentional, so we can move focus there. + Unused << bc->GetDocument(); + } + } + } + + return GetTheFocusableArea(aElement, aFlags); +} + +bool nsFocusManager::Blur(BrowsingContext* aBrowsingContextToClear, + BrowsingContext* aAncestorBrowsingContextToFocus, + bool aIsLeavingDocument, bool aAdjustWidget, + bool aRemainActive, uint64_t aActionId, + Element* aElementToFocus) { + if (XRE_IsParentProcess()) { + return BlurImpl(aBrowsingContextToClear, aAncestorBrowsingContextToFocus, + aIsLeavingDocument, aAdjustWidget, aRemainActive, + aElementToFocus, aActionId); + } + mozilla::dom::ContentChild* contentChild = + mozilla::dom::ContentChild::GetSingleton(); + MOZ_ASSERT(contentChild); + bool windowToClearHandled = false; + bool ancestorWindowToFocusHandled = false; + + RefPtr<BrowsingContext> focusedBrowsingContext = GetFocusedBrowsingContext(); + if (focusedBrowsingContext && focusedBrowsingContext->IsDiscarded()) { + focusedBrowsingContext = nullptr; + } + if (!focusedBrowsingContext) { + mFocusedElement = nullptr; + return true; + } + if (aBrowsingContextToClear && aBrowsingContextToClear->IsDiscarded()) { + aBrowsingContextToClear = nullptr; + } + if (aAncestorBrowsingContextToFocus && + aAncestorBrowsingContextToFocus->IsDiscarded()) { + aAncestorBrowsingContextToFocus = nullptr; + } + // XXX should more early returns from BlurImpl be hoisted here to avoid + // processing aBrowsingContextToClear and aAncestorBrowsingContextToFocus in + // other processes when BlurImpl returns early in this process? Or should the + // IPC messages for those be sent by BlurImpl itself, in which case they could + // arrive late? + if (focusedBrowsingContext->IsInProcess()) { + if (aBrowsingContextToClear && !aBrowsingContextToClear->IsInProcess()) { + MOZ_RELEASE_ASSERT(!(aAncestorBrowsingContextToFocus && + !aAncestorBrowsingContextToFocus->IsInProcess()), + "Both aBrowsingContextToClear and " + "aAncestorBrowsingContextToFocus are " + "out-of-process."); + contentChild->SendSetFocusedElement(aBrowsingContextToClear, false); + } + if (aAncestorBrowsingContextToFocus && + !aAncestorBrowsingContextToFocus->IsInProcess()) { + contentChild->SendSetFocusedElement(aAncestorBrowsingContextToFocus, + true); + } + return BlurImpl(aBrowsingContextToClear, aAncestorBrowsingContextToFocus, + aIsLeavingDocument, aAdjustWidget, aRemainActive, + aElementToFocus, aActionId); + } + if (aBrowsingContextToClear && aBrowsingContextToClear->IsInProcess()) { + nsPIDOMWindowOuter* windowToClear = aBrowsingContextToClear->GetDOMWindow(); + MOZ_ASSERT(windowToClear); + windowToClear->SetFocusedElement(nullptr); + windowToClearHandled = true; + } + if (aAncestorBrowsingContextToFocus && + aAncestorBrowsingContextToFocus->IsInProcess()) { + nsPIDOMWindowOuter* ancestorWindowToFocus = + aAncestorBrowsingContextToFocus->GetDOMWindow(); + MOZ_ASSERT(ancestorWindowToFocus); + ancestorWindowToFocus->SetFocusedElement(nullptr, 0, true); + ancestorWindowToFocusHandled = true; + } + // The expectation is that the blurring would eventually result in an IPC + // message doing this anyway, but this doesn't happen if the focus is in OOP + // iframe which won't try to bounce an IPC message to its parent frame. + SetFocusedWindowInternal(nullptr, aActionId); + contentChild->SendBlurToParent( + focusedBrowsingContext, aBrowsingContextToClear, + aAncestorBrowsingContextToFocus, aIsLeavingDocument, aAdjustWidget, + windowToClearHandled, ancestorWindowToFocusHandled, aActionId); + return true; +} + +void nsFocusManager::BlurFromOtherProcess( + mozilla::dom::BrowsingContext* aFocusedBrowsingContext, + mozilla::dom::BrowsingContext* aBrowsingContextToClear, + mozilla::dom::BrowsingContext* aAncestorBrowsingContextToFocus, + bool aIsLeavingDocument, bool aAdjustWidget, uint64_t aActionId) { + if (aFocusedBrowsingContext != GetFocusedBrowsingContext()) { + return; + } + BlurImpl(aBrowsingContextToClear, aAncestorBrowsingContextToFocus, + aIsLeavingDocument, aAdjustWidget, /* aRemainActive = */ false, + nullptr, aActionId); +} + +bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear, + BrowsingContext* aAncestorBrowsingContextToFocus, + bool aIsLeavingDocument, bool aAdjustWidget, + bool aRemainActive, Element* aElementToFocus, + uint64_t aActionId) { + LOGFOCUS(("<<Blur begin actionid: %" PRIu64 ">>", aActionId)); + + // hold a reference to the focused content, which may be null + RefPtr<Element> element = mFocusedElement; + if (element) { + if (!element->IsInComposedDoc()) { + mFocusedElement = nullptr; + return true; + } + if (element == mFirstBlurEvent) { + return true; + } + } + + RefPtr<BrowsingContext> focusedBrowsingContext = GetFocusedBrowsingContext(); + // hold a reference to the focused window + nsCOMPtr<nsPIDOMWindowOuter> window; + if (focusedBrowsingContext) { + window = focusedBrowsingContext->GetDOMWindow(); + } + if (!window) { + mFocusedElement = nullptr; + return true; + } + + nsCOMPtr<nsIDocShell> docShell = window->GetDocShell(); + if (!docShell) { + if (XRE_IsContentProcess() && + ActionIdComparableAndLower( + aActionId, mActionIdForFocusedBrowsingContextInContent)) { + // Unclear if this ever happens. + LOGFOCUS( + ("Ignored an attempt to null out focused BrowsingContext when " + "docShell is null due to a stale action id %" PRIu64 ".", + aActionId)); + return true; + } + + mFocusedWindow = nullptr; + // Setting focused BrowsingContext to nullptr to avoid leaking in print + // preview. + SetFocusedBrowsingContext(nullptr, aActionId); + mFocusedElement = nullptr; + return true; + } + + // Keep a ref to presShell since dispatching the DOM event may cause + // the document to be destroyed. + RefPtr<PresShell> presShell = docShell->GetPresShell(); + if (!presShell) { + if (XRE_IsContentProcess() && + ActionIdComparableAndLower( + aActionId, mActionIdForFocusedBrowsingContextInContent)) { + // Unclear if this ever happens. + LOGFOCUS( + ("Ignored an attempt to null out focused BrowsingContext when " + "presShell is null due to a stale action id %" PRIu64 ".", + aActionId)); + return true; + } + mFocusedElement = nullptr; + mFocusedWindow = nullptr; + // Setting focused BrowsingContext to nullptr to avoid leaking in print + // preview. + SetFocusedBrowsingContext(nullptr, aActionId); + return true; + } + + Maybe<AutoRestore<RefPtr<Element>>> ar; + if (!mFirstBlurEvent) { + ar.emplace(mFirstBlurEvent); + mFirstBlurEvent = element; + } + + const RefPtr<nsPresContext> focusedPresContext = + GetActiveBrowsingContext() ? presShell->GetPresContext() : nullptr; + IMEStateManager::OnChangeFocus(focusedPresContext, nullptr, + GetFocusMoveActionCause(0)); + + // now adjust the actual focus, by clearing the fields in the focus manager + // and in the window. + mFocusedElement = nullptr; + if (aBrowsingContextToClear) { + nsPIDOMWindowOuter* windowToClear = aBrowsingContextToClear->GetDOMWindow(); + if (windowToClear) { + windowToClear->SetFocusedElement(nullptr); + } + } + + LOGCONTENT("Element %s has been blurred", element.get()); + + // Don't fire blur event on the root content which isn't editable. + bool sendBlurEvent = + element && element->IsInComposedDoc() && !IsNonFocusableRoot(element); + if (element) { + if (sendBlurEvent) { + NotifyFocusStateChange(element, aElementToFocus, 0, false, false); + } + + if (!aRemainActive) { + bool windowBeingLowered = !aBrowsingContextToClear && + !aAncestorBrowsingContextToFocus && + aIsLeavingDocument && aAdjustWidget; + // If the object being blurred is a remote browser, deactivate remote + // content + if (BrowserParent* remote = BrowserParent::GetFrom(element)) { + MOZ_ASSERT(XRE_IsParentProcess()); + // Let's deactivate all remote browsers. + BrowsingContext* topLevelBrowsingContext = remote->GetBrowsingContext(); + topLevelBrowsingContext->PreOrderWalk([&](BrowsingContext* aContext) { + if (WindowGlobalParent* windowGlobalParent = + aContext->Canonical()->GetCurrentWindowGlobal()) { + if (RefPtr<BrowserParent> browserParent = + windowGlobalParent->GetBrowserParent()) { + browserParent->Deactivate(windowBeingLowered, aActionId); + LOGFOCUS( + ("%s remote browser deactivated %p, %d, actionid: %" PRIu64, + aContext == topLevelBrowsingContext ? "Top-level" + : "OOP iframe", + browserParent.get(), windowBeingLowered, aActionId)); + } + } + }); + } + + // Same as above but for out-of-process iframes + if (BrowserBridgeChild* bbc = BrowserBridgeChild::GetFrom(element)) { + bbc->Deactivate(windowBeingLowered, aActionId); + LOGFOCUS( + ("Out-of-process iframe deactivated %p, %d, actionid: %" PRIu64, + bbc, windowBeingLowered, aActionId)); + } + } + } + + bool result = true; + if (sendBlurEvent) { + // if there is an active window, update commands. If there isn't an active + // window, then this was a blur caused by the active window being lowered, + // so there is no need to update the commands + if (GetActiveBrowsingContext()) { + window->UpdateCommands(u"focus"_ns); + } + + SendFocusOrBlurEvent(eBlur, presShell, element->GetComposedDoc(), element, + false, false, aElementToFocus); + } + + // if we are leaving the document or the window was lowered, make the caret + // invisible. + if (aIsLeavingDocument || !GetActiveBrowsingContext()) { + SetCaretVisible(presShell, false, nullptr); + } + + RefPtr<AccessibleCaretEventHub> eventHub = + presShell->GetAccessibleCaretEventHub(); + if (eventHub) { + eventHub->NotifyBlur(aIsLeavingDocument || !GetActiveBrowsingContext()); + } + + // at this point, it is expected that this window will be still be + // focused, but the focused element will be null, as it was cleared before + // the event. If this isn't the case, then something else was focused during + // the blur event above and we should just return. However, if + // aIsLeavingDocument is set, a new document is desired, so make sure to + // blur the document and window. + if (GetFocusedBrowsingContext() != window->GetBrowsingContext() || + (mFocusedElement != nullptr && !aIsLeavingDocument)) { + result = false; + } else if (aIsLeavingDocument) { + window->TakeFocus(false, 0); + + // clear the focus so that the ancestor frame hierarchy is in the correct + // state. Pass true because aAncestorBrowsingContextToFocus is thought to be + // focused at this point. + if (aAncestorBrowsingContextToFocus) { + nsPIDOMWindowOuter* ancestorWindowToFocus = + aAncestorBrowsingContextToFocus->GetDOMWindow(); + if (ancestorWindowToFocus) { + ancestorWindowToFocus->SetFocusedElement(nullptr, 0, true); + } + } + + SetFocusedWindowInternal(nullptr, aActionId); + mFocusedElement = nullptr; + + RefPtr<Document> doc = window->GetExtantDoc(); + if (doc) { + SendFocusOrBlurEvent(eBlur, presShell, doc, doc, false); + } + if (!GetFocusedBrowsingContext()) { + nsCOMPtr<nsPIDOMWindowInner> innerWindow = + window->GetCurrentInnerWindow(); + // MOZ_KnownLive due to bug 1506441 + SendFocusOrBlurEvent( + eBlur, presShell, doc, + MOZ_KnownLive(nsGlobalWindowInner::Cast(innerWindow)), false); + } + + // check if a different window was focused + result = (!GetFocusedBrowsingContext() && GetActiveBrowsingContext()); + } else if (GetActiveBrowsingContext()) { + // Otherwise, the blur of the element without blurring the document + // occurred normally. Call UpdateCaret to redisplay the caret at the right + // location within the document. This is needed to ensure that the caret + // used for caret browsing is made visible again when an input field is + // blurred. + UpdateCaret(false, true, nullptr); + } + + return result; +} + +void nsFocusManager::ActivateRemoteFrameIfNeeded(Element& aElement, + uint64_t aActionId) { + if (BrowserParent* remote = BrowserParent::GetFrom(&aElement)) { + remote->Activate(aActionId); + LOGFOCUS( + ("Remote browser activated %p, actionid: %" PRIu64, remote, aActionId)); + } + + // Same as above but for out-of-process iframes + if (BrowserBridgeChild* bbc = BrowserBridgeChild::GetFrom(&aElement)) { + bbc->Activate(aActionId); + LOGFOCUS(("Out-of-process iframe activated %p, actionid: %" PRIu64, bbc, + aActionId)); + } +} + +void nsFocusManager::Focus( + nsPIDOMWindowOuter* aWindow, Element* aElement, uint32_t aFlags, + bool aIsNewDocument, bool aFocusChanged, bool aWindowRaised, + bool aAdjustWidget, uint64_t aActionId, + const Maybe<BlurredElementInfo>& aBlurredElementInfo) { + LOGFOCUS(("<<Focus begin actionid: %" PRIu64 ">>", aActionId)); + + if (!aWindow) { + return; + } + + if (aElement && + (aElement == mFirstFocusEvent || aElement == mFirstBlurEvent)) { + return; + } + + // Keep a reference to the presShell since dispatching the DOM event may + // cause the document to be destroyed. + nsCOMPtr<nsIDocShell> docShell = aWindow->GetDocShell(); + if (!docShell) { + return; + } + + const RefPtr<PresShell> presShell = docShell->GetPresShell(); + if (!presShell) { + return; + } + + bool focusInOtherContentProcess = false; + // Keep mochitest-browser-chrome harness happy by ignoring + // focusInOtherContentProcess in the chrome process, because the harness + // expects that. + if (!XRE_IsParentProcess()) { + if (RefPtr<nsFrameLoaderOwner> flo = do_QueryObject(aElement)) { + // Only look at pre-existing browsing contexts. If this function is + // called during reflow, calling GetBrowsingContext() could cause frame + // loader initialization at a time when it isn't safe. + if (BrowsingContext* bc = flo->GetExtantBrowsingContext()) { + focusInOtherContentProcess = !bc->IsInProcess(); + } + } + + if (ActionIdComparableAndLower( + aActionId, mActionIdForFocusedBrowsingContextInContent)) { + // Unclear if this ever happens. + LOGFOCUS( + ("Ignored an attempt to focus an element due to stale action id " + "%" PRIu64 ".", + aActionId)); + return; + } + } + + // If the focus actually changed, set the focus method (mouse, keyboard, etc). + // Otherwise, just get the current focus method and use that. This ensures + // that the method is set during the document and window focus events. + uint32_t focusMethod = aFocusChanged + ? aFlags & METHODANDRING_MASK + : aWindow->GetFocusMethod() | + (aFlags & (FLAG_SHOWRING | FLAG_NOSHOWRING)); + + if (!IsWindowVisible(aWindow)) { + // if the window isn't visible, for instance because it is a hidden tab, + // update the current focus and scroll it into view but don't do anything + // else + if (RefPtr elementToFocus = FlushAndCheckIfFocusable(aElement, aFlags)) { + aWindow->SetFocusedElement(elementToFocus, focusMethod); + if (aFocusChanged) { + ScrollIntoView(presShell, elementToFocus, aFlags); + } + } + return; + } + + Maybe<AutoRestore<RefPtr<Element>>> ar; + if (!mFirstFocusEvent) { + ar.emplace(mFirstFocusEvent); + mFirstFocusEvent = aElement; + } + + LOGCONTENT("Element %s has been focused", aElement); + + if (MOZ_LOG_TEST(gFocusLog, LogLevel::Debug)) { + Document* docm = aWindow->GetExtantDoc(); + if (docm) { + LOGCONTENT(" from %s", docm->GetRootElement()); + } + LOGFOCUS( + (" [Newdoc: %d FocusChanged: %d Raised: %d Flags: %x actionid: %" PRIu64 + "]", + aIsNewDocument, aFocusChanged, aWindowRaised, aFlags, aActionId)); + } + + if (aIsNewDocument) { + // if this is a new document, update the parent chain of frames so that + // focus can be traversed from the top level down to the newly focused + // window. + RefPtr<BrowsingContext> bc = aWindow->GetBrowsingContext(); + AdjustWindowFocus(bc, false, IsWindowVisible(aWindow), aActionId); + } + + // indicate that the window has taken focus. + if (aWindow->TakeFocus(true, focusMethod)) { + aIsNewDocument = true; + } + + SetFocusedWindowInternal(aWindow, aActionId); + + if (aAdjustWidget && !sTestMode) { + if (nsViewManager* vm = presShell->GetViewManager()) { + nsCOMPtr<nsIWidget> widget = vm->GetRootWidget(); + if (widget) + widget->SetFocus(nsIWidget::Raise::No, aFlags & FLAG_NONSYSTEMCALLER + ? CallerType::NonSystem + : CallerType::System); + } + } + + // if switching to a new document, first fire the focus event on the + // document and then the window. + if (aIsNewDocument) { + RefPtr<Document> doc = aWindow->GetExtantDoc(); + // The focus change should be notified to IMEStateManager from here if: + // * the focused element is in design mode or + // * nobody gets focus and the document is in design mode + // since any element whose uncomposed document is in design mode won't + // receive focus event. + if (doc && ((aElement && aElement->IsInDesignMode()) || + (!aElement && doc->IsInDesignMode()))) { + RefPtr<nsPresContext> presContext = presShell->GetPresContext(); + IMEStateManager::OnChangeFocus(presContext, nullptr, + GetFocusMoveActionCause(aFlags)); + } + if (doc && !focusInOtherContentProcess) { + SendFocusOrBlurEvent(eFocus, presShell, doc, doc, aWindowRaised); + } + if (GetFocusedBrowsingContext() == aWindow->GetBrowsingContext() && + !mFocusedElement && !focusInOtherContentProcess) { + nsCOMPtr<nsPIDOMWindowInner> innerWindow = + aWindow->GetCurrentInnerWindow(); + // MOZ_KnownLive due to bug 1506441 + SendFocusOrBlurEvent( + eFocus, presShell, doc, + MOZ_KnownLive(nsGlobalWindowInner::Cast(innerWindow)), aWindowRaised); + } + } + + // check to ensure that the element is still focusable, and that nothing + // else was focused during the events above. + // Note that the focusing element may have already been moved to another + // document/window. In that case, we should stop setting focus to it + // because setting focus to the new window would cause redirecting focus + // again and again. + RefPtr elementToFocus = + aElement && aElement->IsInComposedDoc() && + aElement->GetComposedDoc() == aWindow->GetExtantDoc() + ? FlushAndCheckIfFocusable(aElement, aFlags) + : nullptr; + if (elementToFocus && !mFocusedElement && + GetFocusedBrowsingContext() == aWindow->GetBrowsingContext()) { + mFocusedElement = elementToFocus; + + nsIContent* focusedNode = aWindow->GetFocusedElement(); + const bool sendFocusEvent = elementToFocus->IsInComposedDoc() && + !IsNonFocusableRoot(elementToFocus); + const bool isRefocus = focusedNode && focusedNode == elementToFocus; + const bool shouldShowFocusRing = + sendFocusEvent && + ShouldMatchFocusVisible(aWindow, *elementToFocus, aFlags); + + aWindow->SetFocusedElement(elementToFocus, focusMethod, false); + + const RefPtr<nsPresContext> presContext = presShell->GetPresContext(); + if (sendFocusEvent) { + NotifyFocusStateChange(elementToFocus, nullptr, aFlags, + /* aGettingFocus = */ true, shouldShowFocusRing); + + // If this is a remote browser, focus its widget and activate remote + // content. Note that we might no longer be in the same document, + // due to the events we fired above when aIsNewDocument. + if (presShell->GetDocument() == elementToFocus->GetComposedDoc()) { + ActivateRemoteFrameIfNeeded(*elementToFocus, aActionId); + } + + IMEStateManager::OnChangeFocus(presContext, elementToFocus, + GetFocusMoveActionCause(aFlags)); + + // as long as this focus wasn't because a window was raised, update the + // commands + // XXXndeakin P2 someone could adjust the focus during the update + if (!aWindowRaised) { + aWindow->UpdateCommands(u"focus"_ns); + } + + // If the focused element changed, scroll it into view + if (aFocusChanged) { + ScrollIntoView(presShell, elementToFocus, aFlags); + } + + if (!focusInOtherContentProcess) { + RefPtr<Document> composedDocument = elementToFocus->GetComposedDoc(); + RefPtr<Element> relatedTargetElement = + aBlurredElementInfo ? aBlurredElementInfo->mElement.get() : nullptr; + SendFocusOrBlurEvent(eFocus, presShell, composedDocument, + elementToFocus, aWindowRaised, isRefocus, + relatedTargetElement); + } + } else { + // We should notify IMEStateManager of actual focused element even if it + // won't get focus event because the other IMEStateManager users do not + // want to depend on this check, but IMEStateManager wants to verify + // passed focused element for avoidng to overrride nested calls. + IMEStateManager::OnChangeFocus(presContext, elementToFocus, + GetFocusMoveActionCause(aFlags)); + if (!aWindowRaised) { + aWindow->UpdateCommands(u"focus"_ns); + } + if (aFocusChanged) { + // If the focused element changed, scroll it into view + ScrollIntoView(presShell, elementToFocus, aFlags); + } + } + } else { + if (!mFocusedElement && mFocusedWindow == aWindow) { + // When there is no focused element, IMEStateManager needs to adjust IME + // enabled state with the document. + RefPtr<nsPresContext> presContext = presShell->GetPresContext(); + IMEStateManager::OnChangeFocus(presContext, nullptr, + GetFocusMoveActionCause(aFlags)); + } + + if (!aWindowRaised) { + aWindow->UpdateCommands(u"focus"_ns); + } + } + + // update the caret visibility and position to match the newly focused + // element. However, don't update the position if this was a focus due to a + // mouse click as the selection code would already have moved the caret as + // needed. If this is a different document than was focused before, also + // update the caret's visibility. If this is the same document, the caret + // visibility should be the same as before so there is no need to update it. + if (mFocusedElement == elementToFocus) { + RefPtr<Element> focusedElement = mFocusedElement; + UpdateCaret(aFocusChanged && !(aFlags & FLAG_BYMOUSE), aIsNewDocument, + focusedElement); + } +} + +class FocusBlurEvent : public Runnable { + public: + FocusBlurEvent(EventTarget* aTarget, EventMessage aEventMessage, + nsPresContext* aContext, bool aWindowRaised, bool aIsRefocus, + EventTarget* aRelatedTarget) + : mozilla::Runnable("FocusBlurEvent"), + mTarget(aTarget), + mContext(aContext), + mEventMessage(aEventMessage), + mWindowRaised(aWindowRaised), + mIsRefocus(aIsRefocus), + mRelatedTarget(aRelatedTarget) {} + + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398) + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { + InternalFocusEvent event(true, mEventMessage); + event.mFlags.mBubbles = false; + event.mFlags.mCancelable = false; + event.mFromRaise = mWindowRaised; + event.mIsRefocus = mIsRefocus; + event.mRelatedTarget = mRelatedTarget; + return EventDispatcher::Dispatch(mTarget, mContext, &event); + } + + const nsCOMPtr<EventTarget> mTarget; + const RefPtr<nsPresContext> mContext; + EventMessage mEventMessage; + bool mWindowRaised; + bool mIsRefocus; + nsCOMPtr<EventTarget> mRelatedTarget; +}; + +class FocusInOutEvent : public Runnable { + public: + FocusInOutEvent(EventTarget* aTarget, EventMessage aEventMessage, + nsPresContext* aContext, + nsPIDOMWindowOuter* aOriginalFocusedWindow, + nsIContent* aOriginalFocusedContent, + EventTarget* aRelatedTarget) + : mozilla::Runnable("FocusInOutEvent"), + mTarget(aTarget), + mContext(aContext), + mEventMessage(aEventMessage), + mOriginalFocusedWindow(aOriginalFocusedWindow), + mOriginalFocusedContent(aOriginalFocusedContent), + mRelatedTarget(aRelatedTarget) {} + + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398) + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { + nsCOMPtr<nsIContent> originalWindowFocus = + mOriginalFocusedWindow ? mOriginalFocusedWindow->GetFocusedElement() + : nullptr; + // Blink does not check that focus is the same after blur, but WebKit does. + // Opt to follow Blink's behavior (see bug 687787). + if (mEventMessage == eFocusOut || + originalWindowFocus == mOriginalFocusedContent) { + InternalFocusEvent event(true, mEventMessage); + event.mFlags.mBubbles = true; + event.mFlags.mCancelable = false; + event.mRelatedTarget = mRelatedTarget; + return EventDispatcher::Dispatch(mTarget, mContext, &event); + } + return NS_OK; + } + + const nsCOMPtr<EventTarget> mTarget; + const RefPtr<nsPresContext> mContext; + EventMessage mEventMessage; + nsCOMPtr<nsPIDOMWindowOuter> mOriginalFocusedWindow; + nsCOMPtr<nsIContent> mOriginalFocusedContent; + nsCOMPtr<EventTarget> mRelatedTarget; +}; + +static Document* GetDocumentHelper(EventTarget* aTarget) { + if (!aTarget) { + return nullptr; + } + if (const nsINode* node = nsINode::FromEventTarget(aTarget)) { + return node->OwnerDoc(); + } + nsPIDOMWindowInner* win = nsPIDOMWindowInner::FromEventTarget(aTarget); + return win ? win->GetExtantDoc() : nullptr; +} + +void nsFocusManager::FireFocusInOrOutEvent( + EventMessage aEventMessage, PresShell* aPresShell, EventTarget* aTarget, + nsPIDOMWindowOuter* aCurrentFocusedWindow, + nsIContent* aCurrentFocusedContent, EventTarget* aRelatedTarget) { + NS_ASSERTION(aEventMessage == eFocusIn || aEventMessage == eFocusOut, + "Wrong event type for FireFocusInOrOutEvent"); + + nsContentUtils::AddScriptRunner(new FocusInOutEvent( + aTarget, aEventMessage, aPresShell->GetPresContext(), + aCurrentFocusedWindow, aCurrentFocusedContent, aRelatedTarget)); +} + +void nsFocusManager::SendFocusOrBlurEvent(EventMessage aEventMessage, + PresShell* aPresShell, + Document* aDocument, + EventTarget* aTarget, + bool aWindowRaised, bool aIsRefocus, + EventTarget* aRelatedTarget) { + NS_ASSERTION(aEventMessage == eFocus || aEventMessage == eBlur, + "Wrong event type for SendFocusOrBlurEvent"); + + nsCOMPtr<Document> eventTargetDoc = GetDocumentHelper(aTarget); + nsCOMPtr<Document> relatedTargetDoc = GetDocumentHelper(aRelatedTarget); + + // set aRelatedTarget to null if it's not in the same document as aTarget + if (eventTargetDoc != relatedTargetDoc) { + aRelatedTarget = nullptr; + } + + if (aDocument && aDocument->EventHandlingSuppressed()) { + // if this event was already queued, remove it and append it to the end + mDelayedBlurFocusEvents.RemoveElementsBy([&](const auto& event) { + return event.mEventMessage == aEventMessage && + event.mPresShell == aPresShell && event.mDocument == aDocument && + event.mTarget == aTarget && event.mRelatedTarget == aRelatedTarget; + }); + + mDelayedBlurFocusEvents.EmplaceBack(aEventMessage, aPresShell, aDocument, + aTarget, aRelatedTarget); + return; + } + + // If mDelayedBlurFocusEvents queue is not empty, check if there are events + // that belongs to this doc, if yes, fire them first. + if (aDocument && !aDocument->EventHandlingSuppressed() && + mDelayedBlurFocusEvents.Length()) { + FireDelayedEvents(aDocument); + } + + FireFocusOrBlurEvent(aEventMessage, aPresShell, aTarget, aWindowRaised, + aIsRefocus, aRelatedTarget); +} + +void nsFocusManager::FireFocusOrBlurEvent(EventMessage aEventMessage, + PresShell* aPresShell, + EventTarget* aTarget, + bool aWindowRaised, bool aIsRefocus, + EventTarget* aRelatedTarget) { + nsCOMPtr<nsPIDOMWindowOuter> currentWindow = mFocusedWindow; + nsCOMPtr<nsPIDOMWindowInner> targetWindow = do_QueryInterface(aTarget); + nsCOMPtr<Document> targetDocument = do_QueryInterface(aTarget); + nsCOMPtr<nsIContent> currentFocusedContent = + currentWindow ? currentWindow->GetFocusedElement() : nullptr; + +#ifdef ACCESSIBILITY + nsAccessibilityService* accService = GetAccService(); + if (accService) { + if (aEventMessage == eFocus) { + accService->NotifyOfDOMFocus(aTarget); + } else { + accService->NotifyOfDOMBlur(aTarget); + } + } +#endif + + aPresShell->ScheduleContentRelevancyUpdate( + ContentRelevancyReason::FocusInSubtree); + + nsContentUtils::AddScriptRunner( + new FocusBlurEvent(aTarget, aEventMessage, aPresShell->GetPresContext(), + aWindowRaised, aIsRefocus, aRelatedTarget)); + + // Check that the target is not a window or document before firing + // focusin/focusout. Other browsers do not fire focusin/focusout on window, + // despite being required in the spec, so follow their behavior. + // + // As for document, we should not even fire focus/blur, but until then, we + // need this check. targetDocument should be removed once bug 1228802 is + // resolved. + if (!targetWindow && !targetDocument) { + EventMessage focusInOrOutMessage = + aEventMessage == eFocus ? eFocusIn : eFocusOut; + FireFocusInOrOutEvent(focusInOrOutMessage, aPresShell, aTarget, + currentWindow, currentFocusedContent, aRelatedTarget); + } +} + +void nsFocusManager::ScrollIntoView(PresShell* aPresShell, nsIContent* aContent, + uint32_t aFlags) { + if (aFlags & FLAG_NOSCROLL) { + return; + } + + // If the noscroll flag isn't set, scroll the newly focused element into view. + const ScrollAxis axis(WhereToScroll::Center, WhenToScroll::IfNotVisible); + aPresShell->ScrollContentIntoView(aContent, axis, axis, + ScrollFlags::ScrollOverflowHidden); + // Scroll the input / textarea selection into view, unless focused with the + // mouse, see bug 572649. + if (aFlags & FLAG_BYMOUSE) { + return; + } + // ScrollContentIntoView flushes layout, so no need to flush again here. + if (nsTextControlFrame* tf = do_QueryFrame(aContent->GetPrimaryFrame())) { + tf->ScrollSelectionIntoViewAsync(nsTextControlFrame::ScrollAncestors::Yes); + } +} + +void nsFocusManager::RaiseWindow(nsPIDOMWindowOuter* aWindow, + CallerType aCallerType, uint64_t aActionId) { + // don't raise windows that are already raised or are in the process of + // being lowered + + if (!aWindow || aWindow == mWindowBeingLowered) { + return; + } + + if (XRE_IsParentProcess()) { + if (aWindow == mActiveWindow) { + return; + } + } else { + BrowsingContext* bc = aWindow->GetBrowsingContext(); + // TODO: Deeper OOP frame hierarchies are + // https://bugzilla.mozilla.org/show_bug.cgi?id=1661227 + if (bc == GetActiveBrowsingContext()) { + return; + } + if (bc == GetFocusedBrowsingContext()) { + return; + } + } + + if (sTestMode) { + // In test mode, emulate raising the window. WindowRaised takes + // care of lowering the present active window. This happens in + // a separate runnable to avoid touching multiple windows in + // the current runnable. + + nsCOMPtr<nsPIDOMWindowOuter> window(aWindow); + RefPtr<nsFocusManager> self(this); + NS_DispatchToCurrentThread(NS_NewRunnableFunction( + "nsFocusManager::RaiseWindow", + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1770093) + [self, window]() MOZ_CAN_RUN_SCRIPT_BOUNDARY -> void { + self->WindowRaised(window, GenerateFocusActionId()); + })); + return; + } + + if (XRE_IsContentProcess()) { + BrowsingContext* bc = aWindow->GetBrowsingContext(); + if (!bc->IsTop()) { + // Assume the raise below will succeed and run the raising synchronously + // in this process to make the focus event that is observable in this + // process fire in the right order relative to mouseup when we are here + // thanks to a mousedown. + WindowRaised(aWindow, aActionId); + } + } + +#if defined(XP_WIN) + // Windows would rather we focus the child widget, otherwise, the toplevel + // widget will always end up being focused. Fortunately, focusing the child + // widget will also have the effect of raising the window this widget is in. + // But on other platforms, we can just focus the toplevel widget to raise + // the window. + nsCOMPtr<nsPIDOMWindowOuter> childWindow; + GetFocusedDescendant(aWindow, eIncludeAllDescendants, + getter_AddRefs(childWindow)); + if (!childWindow) { + childWindow = aWindow; + } + + nsCOMPtr<nsIDocShell> docShell = aWindow->GetDocShell(); + if (!docShell) { + return; + } + + PresShell* presShell = docShell->GetPresShell(); + if (!presShell) { + return; + } + + if (nsViewManager* vm = presShell->GetViewManager()) { + nsCOMPtr<nsIWidget> widget = vm->GetRootWidget(); + if (widget) { + widget->SetFocus(nsIWidget::Raise::Yes, aCallerType); + } + } +#else + nsCOMPtr<nsIBaseWindow> treeOwnerAsWin = + do_QueryInterface(aWindow->GetDocShell()); + if (treeOwnerAsWin) { + nsCOMPtr<nsIWidget> widget; + treeOwnerAsWin->GetMainWidget(getter_AddRefs(widget)); + if (widget) { + widget->SetFocus(nsIWidget::Raise::Yes, aCallerType); + } + } +#endif +} + +void nsFocusManager::UpdateCaretForCaretBrowsingMode() { + RefPtr<Element> focusedElement = mFocusedElement; + UpdateCaret(false, true, focusedElement); +} + +void nsFocusManager::UpdateCaret(bool aMoveCaretToFocus, bool aUpdateVisibility, + nsIContent* aContent) { + LOGFOCUS(("Update Caret: %d %d", aMoveCaretToFocus, aUpdateVisibility)); + + if (!mFocusedWindow) { + return; + } + + // this is called when a document is focused or when the caretbrowsing + // preference is changed + nsCOMPtr<nsIDocShell> focusedDocShell = mFocusedWindow->GetDocShell(); + if (!focusedDocShell) { + return; + } + + if (focusedDocShell->ItemType() == nsIDocShellTreeItem::typeChrome) { + return; // Never browse with caret in chrome + } + + bool browseWithCaret = Preferences::GetBool("accessibility.browsewithcaret"); + + const RefPtr<PresShell> presShell = focusedDocShell->GetPresShell(); + if (!presShell) { + return; + } + + // If this is an editable document which isn't contentEditable, or a + // contentEditable document and the node to focus is contentEditable, + // return, so that we don't mess with caret visibility. + bool isEditable = false; + focusedDocShell->GetEditable(&isEditable); + + if (isEditable) { + Document* doc = presShell->GetDocument(); + + bool isContentEditableDoc = + doc && + doc->GetEditingState() == Document::EditingState::eContentEditable; + + bool isFocusEditable = aContent && aContent->HasFlag(NODE_IS_EDITABLE); + if (!isContentEditableDoc || isFocusEditable) { + return; + } + } + + if (!isEditable && aMoveCaretToFocus) { + MoveCaretToFocus(presShell, aContent); + } + + // The above MoveCaretToFocus call may run scripts which + // may clear mFocusWindow + if (!mFocusedWindow) { + return; + } + + if (!aUpdateVisibility) { + return; + } + + // XXXndeakin this doesn't seem right. It should be checking for this only + // on the nearest ancestor frame which is a chrome frame. But this is + // what the existing code does, so just leave it for now. + if (!browseWithCaret) { + nsCOMPtr<Element> docElement = mFocusedWindow->GetFrameElementInternal(); + if (docElement) + browseWithCaret = docElement->AttrValueIs( + kNameSpaceID_None, nsGkAtoms::showcaret, u"true"_ns, eCaseMatters); + } + + SetCaretVisible(presShell, browseWithCaret, aContent); +} + +void nsFocusManager::MoveCaretToFocus(PresShell* aPresShell, + nsIContent* aContent) { + nsCOMPtr<Document> doc = aPresShell->GetDocument(); + if (doc) { + RefPtr<nsFrameSelection> frameSelection = aPresShell->FrameSelection(); + RefPtr<Selection> domSelection = + frameSelection->GetSelection(SelectionType::eNormal); + if (domSelection) { + // First clear the selection. This way, if there is no currently focused + // content, the selection will just be cleared. + domSelection->RemoveAllRanges(IgnoreErrors()); + if (aContent) { + ErrorResult rv; + RefPtr<nsRange> newRange = doc->CreateRange(rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + return; + } + + // Set the range to the start of the currently focused node + // Make sure it's collapsed + newRange->SelectNodeContents(*aContent, IgnoreErrors()); + + if (!aContent->GetFirstChild() || + aContent->IsHTMLFormControlElement()) { + // If current focus node is a leaf, set range to before the + // node by using the parent as a container. + // This prevents it from appearing as selected. + newRange->SetStartBefore(*aContent, IgnoreErrors()); + newRange->SetEndBefore(*aContent, IgnoreErrors()); + } + domSelection->AddRangeAndSelectFramesAndNotifyListeners(*newRange, + IgnoreErrors()); + domSelection->CollapseToStart(IgnoreErrors()); + } + } + } +} + +nsresult nsFocusManager::SetCaretVisible(PresShell* aPresShell, bool aVisible, + nsIContent* aContent) { + // When browsing with caret, make sure caret is visible after new focus + // Return early if there is no caret. This can happen for the testcase + // for bug 308025 where a window is closed in a blur handler. + RefPtr<nsCaret> caret = aPresShell->GetCaret(); + if (!caret) { + return NS_OK; + } + + bool caretVisible = caret->IsVisible(); + if (!aVisible && !caretVisible) { + return NS_OK; + } + + RefPtr<nsFrameSelection> frameSelection; + if (aContent) { + NS_ASSERTION(aContent->GetComposedDoc() == aPresShell->GetDocument(), + "Wrong document?"); + nsIFrame* focusFrame = aContent->GetPrimaryFrame(); + if (focusFrame) { + frameSelection = focusFrame->GetFrameSelection(); + } + } + + RefPtr<nsFrameSelection> docFrameSelection = aPresShell->FrameSelection(); + + if (docFrameSelection && caret && + (frameSelection == docFrameSelection || !aContent)) { + Selection* domSelection = + docFrameSelection->GetSelection(SelectionType::eNormal); + if (domSelection) { + // First, hide the caret to prevent attempting to show it in + // SetCaretDOMSelection + aPresShell->SetCaretEnabled(false); + + // Caret must blink on non-editable elements + caret->SetIgnoreUserModify(true); + // Tell the caret which selection to use + caret->SetSelection(domSelection); + + // In content, we need to set the caret. The only special case is edit + // fields, which have a different frame selection from the document. + // They will take care of making the caret visible themselves. + + aPresShell->SetCaretReadOnly(false); + aPresShell->SetCaretEnabled(aVisible); + } + } + + return NS_OK; +} + +void nsFocusManager::GetSelectionLocation(Document* aDocument, + PresShell* aPresShell, + nsIContent** aStartContent, + nsIContent** aEndContent) { + *aStartContent = *aEndContent = nullptr; + + nsPresContext* presContext = aPresShell->GetPresContext(); + NS_ASSERTION(presContext, "mPresContent is null!!"); + + RefPtr<Selection> domSelection = + aPresShell->ConstFrameSelection()->GetSelection(SelectionType::eNormal); + if (!domSelection) { + return; + } + + const nsRange* domRange = domSelection->GetRangeAt(0); + if (!domRange || !domRange->IsPositioned()) { + return; + } + nsIContent* start = nsIContent::FromNode(domRange->GetStartContainer()); + nsIContent* end = nsIContent::FromNode(domRange->GetEndContainer()); + if (nsIContent* child = domRange->StartRef().GetChildAtOffset()) { + start = child; + } + if (nsIContent* child = domRange->EndRef().GetChildAtOffset()) { + end = child; + } + + // Next check to see if our caret is at the very end of a text node. If so, + // the caret is actually sitting in front of the next logical frame's primary + // node - so for this case we need to change the content to that node. + // Note that if the text does not have text frame, we do not need to retreive + // caret frame. This could occur if text frame has only collapsisble white- + // spaces and is around a block boundary or an ancestor of it is invisible. + // XXX If there is a visible text sibling, should we return it in the former + // case? + if (auto* text = Text::FromNodeOrNull(start); + text && text->GetPrimaryFrame() && + text->TextDataLength() == domRange->StartOffset() && + domSelection->IsCollapsed()) { + nsIFrame* startFrame = start->GetPrimaryFrame(); + // Yes, indeed we were at the end of the last node + nsIFrame* limiter = + domSelection && domSelection->GetAncestorLimiter() + ? domSelection->GetAncestorLimiter()->GetPrimaryFrame() + : nullptr; + nsFrameIterator frameIterator(presContext, startFrame, + nsFrameIterator::Type::Leaf, + false, // aVisual + false, // aLockInScrollView + true, // aFollowOOFs + false, // aSkipPopupChecks + limiter); + + nsIFrame* newCaretFrame = nullptr; + nsIContent* newCaretContent = start; + const bool endOfSelectionInStartNode = start == end; + do { + // Continue getting the next frame until the primary content for the + // frame we are on changes - we don't want to be stuck in the same + // place + frameIterator.Next(); + newCaretFrame = frameIterator.CurrentItem(); + if (!newCaretFrame) { + break; + } + newCaretContent = newCaretFrame->GetContent(); + } while (!newCaretContent || newCaretContent == start); + + if (newCaretFrame && newCaretContent) { + // If the caret is exactly at the same position of the new frame, + // then we can use the newCaretFrame and newCaretContent for our + // position + nsRect caretRect; + if (nsIFrame* frame = nsCaret::GetGeometry(domSelection, &caretRect)) { + nsPoint caretWidgetOffset; + nsIWidget* widget = frame->GetNearestWidget(caretWidgetOffset); + caretRect.MoveBy(caretWidgetOffset); + nsPoint newCaretOffset; + nsIWidget* newCaretWidget = + newCaretFrame->GetNearestWidget(newCaretOffset); + if (widget == newCaretWidget && caretRect.TopLeft() == newCaretOffset) { + // The caret is at the start of the new element. + startFrame = newCaretFrame; + start = newCaretContent; + if (endOfSelectionInStartNode) { + end = newCaretContent; // Ensure end of selection is + // not before start + } + } + } + } + } + + NS_IF_ADDREF(*aStartContent = start); + NS_IF_ADDREF(*aEndContent = end); +} + +nsresult nsFocusManager::DetermineElementToMoveFocus( + nsPIDOMWindowOuter* aWindow, nsIContent* aStartContent, int32_t aType, + bool aNoParentTraversal, bool aNavigateByKey, nsIContent** aNextContent) { + *aNextContent = nullptr; + + // This is used for document navigation only. It will be set to true if we + // start navigating from a starting point. If this starting point is near the + // end of the document (for example, an element on a statusbar), and there + // are no child documents or panels before the end of the document, then we + // will need to ensure that we don't consider the root chrome window when we + // loop around and instead find the next child document/panel, as focus is + // already in that window. This flag will be cleared once we navigate into + // another document. + bool mayFocusRoot = (aStartContent != nullptr); + + nsCOMPtr<nsIContent> startContent = aStartContent; + if (!startContent && aType != MOVEFOCUS_CARET) { + if (aType == MOVEFOCUS_FORWARDDOC || aType == MOVEFOCUS_BACKWARDDOC) { + // When moving between documents, make sure to get the right + // starting content in a descendant. + nsCOMPtr<nsPIDOMWindowOuter> focusedWindow; + startContent = GetFocusedDescendant(aWindow, eIncludeAllDescendants, + getter_AddRefs(focusedWindow)); + } else if (aType != MOVEFOCUS_LASTDOC) { + // Otherwise, start at the focused node. If MOVEFOCUS_LASTDOC is used, + // then we are document-navigating backwards from chrome to the content + // process, and we don't want to use this so that we start from the end + // of the document. + startContent = aWindow->GetFocusedElement(); + } + } + + nsCOMPtr<Document> doc; + if (startContent) + doc = startContent->GetComposedDoc(); + else + doc = aWindow->GetExtantDoc(); + if (!doc) return NS_OK; + + LookAndFeel::GetInt(LookAndFeel::IntID::TabFocusModel, + &nsIContent::sTabFocusModel); + + // True if we are navigating by document (F6/Shift+F6) or false if we are + // navigating by element (Tab/Shift+Tab). + const bool forDocumentNavigation = + aType == MOVEFOCUS_FORWARDDOC || aType == MOVEFOCUS_BACKWARDDOC || + aType == MOVEFOCUS_FIRSTDOC || aType == MOVEFOCUS_LASTDOC; + + // If moving to the root or first document, find the root element and return. + if (aType == MOVEFOCUS_ROOT || aType == MOVEFOCUS_FIRSTDOC) { + NS_IF_ADDREF(*aNextContent = GetRootForFocus(aWindow, doc, false, false)); + if (!*aNextContent && aType == MOVEFOCUS_FIRSTDOC) { + // When looking for the first document, if the root wasn't focusable, + // find the next focusable document. + aType = MOVEFOCUS_FORWARDDOC; + } else { + return NS_OK; + } + } + + // rootElement and presShell may be set to sub-document's ones so that they + // cannot be `const`. + RefPtr<Element> rootElement = doc->GetRootElement(); + NS_ENSURE_TRUE(rootElement, NS_OK); + + RefPtr<PresShell> presShell = doc->GetPresShell(); + NS_ENSURE_TRUE(presShell, NS_OK); + + if (aType == MOVEFOCUS_FIRST) { + if (!aStartContent) { + startContent = rootElement; + } + return GetNextTabbableContent(presShell, startContent, nullptr, + startContent, true, 1, false, false, + aNavigateByKey, false, aNextContent); + } + if (aType == MOVEFOCUS_LAST) { + if (!aStartContent) { + startContent = rootElement; + } + return GetNextTabbableContent(presShell, startContent, nullptr, + startContent, false, 0, false, false, + aNavigateByKey, false, aNextContent); + } + + bool forward = (aType == MOVEFOCUS_FORWARD || aType == MOVEFOCUS_FORWARDDOC || + aType == MOVEFOCUS_CARET); + bool doNavigation = true; + bool ignoreTabIndex = false; + // when a popup is open, we want to ensure that tab navigation occurs only + // within the most recently opened panel. If a popup is open, its frame will + // be stored in popupFrame. + nsIFrame* popupFrame = nullptr; + + int32_t tabIndex = forward ? 1 : 0; + if (startContent) { + nsIFrame* frame = startContent->GetPrimaryFrame(); + tabIndex = (frame && !startContent->IsHTMLElement(nsGkAtoms::area)) + ? frame->IsFocusable().mTabIndex + : startContent->IsFocusableWithoutStyle().mTabIndex; + + // if the current element isn't tabbable, ignore the tabindex and just + // look for the next element. The root content won't have a tabindex + // so just treat this as the beginning of the tab order. + if (tabIndex < 0) { + tabIndex = 1; + if (startContent != rootElement) { + ignoreTabIndex = true; + } + } + + // check if the focus is currently inside a popup. Elements such as the + // autocomplete widget use the noautofocus attribute to allow the focus to + // remain outside the popup when it is opened. + if (frame) { + popupFrame = nsLayoutUtils::GetClosestFrameOfType( + frame, LayoutFrameType::MenuPopup); + } + + if (popupFrame && !forDocumentNavigation) { + // Don't navigate outside of a popup, so pretend that the + // root content is the popup itself + rootElement = popupFrame->GetContent()->AsElement(); + NS_ASSERTION(rootElement, "Popup frame doesn't have a content node"); + } else if (!forward) { + // If focus moves backward and when current focused node is root + // content or <body> element which is editable by contenteditable + // attribute, focus should move to its parent document. + if (startContent == rootElement) { + doNavigation = false; + } else { + Document* doc = startContent->GetComposedDoc(); + if (startContent == + nsLayoutUtils::GetEditableRootContentByContentEditable(doc)) { + doNavigation = false; + } + } + } + } else { + if (aType != MOVEFOCUS_CARET) { + // if there is no focus, yet a panel is open, focus the first item in + // the panel + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (pm) { + popupFrame = pm->GetTopPopup(PopupType::Panel); + } + } + if (popupFrame) { + // When there is a popup open, and no starting content, start the search + // at the topmost popup. + startContent = popupFrame->GetContent(); + NS_ASSERTION(startContent, "Popup frame doesn't have a content node"); + // Unless we are searching for documents, set the root content to the + // popup as well, so that we don't tab-navigate outside the popup. + // When navigating by documents, we start at the popup but can navigate + // outside of it to look for other panels and documents. + if (!forDocumentNavigation) { + rootElement = startContent->AsElement(); + } + + doc = startContent ? startContent->GetComposedDoc() : nullptr; + } else { + // Otherwise, for content shells, start from the location of the caret. + nsCOMPtr<nsIDocShell> docShell = aWindow->GetDocShell(); + if (docShell && docShell->ItemType() != nsIDocShellTreeItem::typeChrome) { + nsCOMPtr<nsIContent> endSelectionContent; + GetSelectionLocation(doc, presShell, getter_AddRefs(startContent), + getter_AddRefs(endSelectionContent)); + // If the selection is on the rootElement, then there is no selection + if (startContent == rootElement) { + startContent = nullptr; + } + + if (aType == MOVEFOCUS_CARET) { + // GetFocusInSelection finds a focusable link near the caret. + // If there is no start content though, don't do this to avoid + // focusing something unexpected. + if (startContent) { + GetFocusInSelection(aWindow, startContent, endSelectionContent, + aNextContent); + } + return NS_OK; + } + + if (startContent) { + // when starting from a selection, we always want to find the next or + // previous element in the document. So the tabindex on elements + // should be ignored. + ignoreTabIndex = true; + } + } + + if (!startContent) { + // otherwise, just use the root content as the starting point + startContent = rootElement; + NS_ENSURE_TRUE(startContent, NS_OK); + } + } + } + + // Check if the starting content is the same as the content assigned to the + // retargetdocumentfocus attribute. Is so, we don't want to start searching + // from there but instead from the beginning of the document. Otherwise, the + // content that appears before the retargetdocumentfocus element will never + // get checked as it will be skipped when the focus is retargetted to it. + if (forDocumentNavigation && nsContentUtils::IsChromeDoc(doc)) { + nsAutoString retarget; + + if (rootElement->GetAttr(nsGkAtoms::retargetdocumentfocus, retarget)) { + nsIContent* retargetElement = doc->GetElementById(retarget); + // The common case here is the urlbar where focus is on the anonymous + // input inside the textbox, but the retargetdocumentfocus attribute + // refers to the textbox. The Contains check will return false and the + // IsInclusiveDescendantOf check will return true in this case. + if (retargetElement && + (retargetElement == startContent || + (!retargetElement->Contains(startContent) && + startContent->IsInclusiveDescendantOf(retargetElement)))) { + startContent = rootElement; + } + } + } + + NS_ASSERTION(startContent, "starting content not set"); + + // keep a reference to the starting content. If we find that again, it means + // we've iterated around completely and we don't want to adjust the focus. + // The skipOriginalContentCheck will be set to true only for the first time + // GetNextTabbableContent is called. This ensures that we don't break out + // when nothing is focused to start with. Specifically, + // GetNextTabbableContent first checks the root content -- which happens to + // be the same as the start content -- when nothing is focused and tabbing + // forward. Without skipOriginalContentCheck set to true, we'd end up + // returning right away and focusing nothing. Luckily, GetNextTabbableContent + // will never wrap around on its own, and can only return the original + // content when it is called a second time or later. + bool skipOriginalContentCheck = true; + const nsCOMPtr<nsIContent> originalStartContent = startContent; + + LOGCONTENTNAVIGATION("Focus Navigation Start Content %s", startContent.get()); + LOGFOCUSNAVIGATION((" Forward: %d Tabindex: %d Ignore: %d DocNav: %d", + forward, tabIndex, ignoreTabIndex, + forDocumentNavigation)); + + while (doc) { + if (doNavigation) { + nsCOMPtr<nsIContent> nextFocus; + // TODO: MOZ_KnownLive is reruired due to bug 1770680 + nsresult rv = GetNextTabbableContent( + presShell, rootElement, + MOZ_KnownLive(skipOriginalContentCheck ? nullptr + : originalStartContent.get()), + startContent, forward, tabIndex, ignoreTabIndex, + forDocumentNavigation, aNavigateByKey, false, + getter_AddRefs(nextFocus)); + NS_ENSURE_SUCCESS(rv, rv); + if (rv == NS_SUCCESS_DOM_NO_OPERATION) { + // Navigation was redirected to a child process, so just return. + return NS_OK; + } + + // found a content node to focus. + if (nextFocus) { + LOGCONTENTNAVIGATION("Next Content: %s", nextFocus.get()); + + // as long as the found node was not the same as the starting node, + // set it as the return value. For document navigation, we can return + // the same element in case there is only one content node that could + // be returned, for example, in a child process document. + if (nextFocus != originalStartContent || forDocumentNavigation) { + nextFocus.forget(aNextContent); + } + return NS_OK; + } + + if (popupFrame && !forDocumentNavigation) { + // in a popup, so start again from the beginning of the popup. However, + // if we already started at the beginning, then there isn't anything to + // focus, so just return + if (startContent != rootElement) { + startContent = rootElement; + tabIndex = forward ? 1 : 0; + continue; + } + return NS_OK; + } + } + + doNavigation = true; + skipOriginalContentCheck = forDocumentNavigation; + ignoreTabIndex = false; + + if (aNoParentTraversal) { + if (startContent == rootElement) { + return NS_OK; + } + + startContent = rootElement; + tabIndex = forward ? 1 : 0; + continue; + } + + // Reached the beginning or end of the document. Next, navigate up to the + // parent document and try again. + nsCOMPtr<nsPIDOMWindowOuter> piWindow = doc->GetWindow(); + NS_ENSURE_TRUE(piWindow, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDocShell> docShell = piWindow->GetDocShell(); + NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE); + + // Get the frame element this window is inside and, from that, get the + // parent document and presshell. If there is no enclosing frame element, + // then this is a top-level, embedded or remote window. + startContent = piWindow->GetFrameElementInternal(); + if (startContent) { + doc = startContent->GetComposedDoc(); + NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE); + + rootElement = doc->GetRootElement(); + presShell = doc->GetPresShell(); + + // We can focus the root element now that we have moved to another + // document. + mayFocusRoot = true; + + nsIFrame* frame = startContent->GetPrimaryFrame(); + if (!frame) { + return NS_OK; + } + + tabIndex = frame->IsFocusable().mTabIndex; + if (tabIndex < 0) { + tabIndex = 1; + ignoreTabIndex = true; + } + + // if the frame is inside a popup, make sure to scan only within the + // popup. This handles the situation of tabbing amongst elements + // inside an iframe which is itself inside a popup. Otherwise, + // navigation would move outside the popup when tabbing outside the + // iframe. + if (!forDocumentNavigation) { + popupFrame = nsLayoutUtils::GetClosestFrameOfType( + frame, LayoutFrameType::MenuPopup); + if (popupFrame) { + rootElement = popupFrame->GetContent()->AsElement(); + NS_ASSERTION(rootElement, "Popup frame doesn't have a content node"); + } + } + } else { + if (aNavigateByKey) { + // There is no parent, so call the tree owner. This will tell the + // embedder or parent process that it should take the focus. + bool tookFocus; + docShell->TabToTreeOwner(forward, forDocumentNavigation, &tookFocus); + // If the tree owner took the focus, blur the current element. + if (tookFocus) { + RefPtr<BrowsingContext> focusedBC = GetFocusedBrowsingContext(); + if (focusedBC && focusedBC->IsInProcess()) { + Blur(focusedBC, nullptr, true, true, false, + GenerateFocusActionId()); + } else { + nsCOMPtr<nsPIDOMWindowOuter> window = docShell->GetWindow(); + window->SetFocusedElement(nullptr); + } + return NS_OK; + } + } + + // If we have reached the end of the top-level document, focus the + // first element in the top-level document. This should always happen + // when navigating by document forwards but when navigating backwards, + // only do this if we started in another document or within a popup frame. + // If the focus started in this window outside a popup however, we should + // continue by looping around to the end again. + if (forDocumentNavigation && (forward || mayFocusRoot || popupFrame)) { + // HTML content documents can have their root element focused (a focus + // ring appears around the entire content area frame). This root + // appears in the tab order before all of the elements in the document. + // Chrome documents however cannot be focused directly, so instead we + // focus the first focusable element within the window. + // For example, the urlbar. + RefPtr<Element> rootElementForFocus = + GetRootForFocus(piWindow, doc, true, true); + return FocusFirst(rootElementForFocus, aNextContent); + } + + // Once we have hit the top-level and have iterated to the end again, we + // just want to break out next time we hit this spot to prevent infinite + // iteration. + mayFocusRoot = true; + + // reset the tab index and start again from the beginning or end + startContent = rootElement; + tabIndex = forward ? 1 : 0; + } + + // wrapped all the way around and didn't find anything to move the focus + // to, so just break out + if (startContent == originalStartContent) { + break; + } + } + + return NS_OK; +} + +uint32_t nsFocusManager::ProgrammaticFocusFlags(const FocusOptions& aOptions) { + uint32_t flags = FLAG_BYJS; + if (aOptions.mPreventScroll) { + flags |= FLAG_NOSCROLL; + } + if (aOptions.mFocusVisible.WasPassed()) { + flags |= aOptions.mFocusVisible.Value() ? FLAG_SHOWRING : FLAG_NOSHOWRING; + } + if (UserActivation::IsHandlingKeyboardInput()) { + flags |= FLAG_BYKEY; + } + // TODO: We could do a similar thing if we're handling mouse input, but that + // changes focusability of some elements so may be more risky. + return flags; +} + +static bool IsHostOrSlot(const nsIContent* aContent) { + return aContent && (aContent->GetShadowRoot() || + aContent->IsHTMLElement(nsGkAtoms::slot)); +} + +// Helper class to iterate contents in scope by traversing flattened tree +// in tree order +class MOZ_STACK_CLASS ScopedContentTraversal { + public: + ScopedContentTraversal(nsIContent* aStartContent, nsIContent* aOwner) + : mCurrent(aStartContent), mOwner(aOwner) { + MOZ_ASSERT(aStartContent); + } + + void Next(); + void Prev(); + + void Reset() { SetCurrent(mOwner); } + + nsIContent* GetCurrent() const { return mCurrent; } + + private: + void SetCurrent(nsIContent* aContent) { mCurrent = aContent; } + + nsIContent* mCurrent; + nsIContent* mOwner; +}; + +void ScopedContentTraversal::Next() { + MOZ_ASSERT(mCurrent); + + // Get mCurrent's first child if it's in the same scope. + if (!IsHostOrSlot(mCurrent) || mCurrent == mOwner) { + StyleChildrenIterator iter(mCurrent); + nsIContent* child = iter.GetNextChild(); + if (child) { + SetCurrent(child); + return; + } + } + + // If mOwner has no children, END traversal + if (mCurrent == mOwner) { + SetCurrent(nullptr); + return; + } + + nsIContent* current = mCurrent; + while (1) { + // Create parent's iterator and move to current + nsIContent* parent = current->GetFlattenedTreeParent(); + StyleChildrenIterator parentIter(parent); + parentIter.Seek(current); + + // Get next sibling of current + if (nsIContent* next = parentIter.GetNextChild()) { + SetCurrent(next); + return; + } + + // If no next sibling and parent is mOwner, END traversal + if (parent == mOwner) { + SetCurrent(nullptr); + return; + } + + current = parent; + } +} + +void ScopedContentTraversal::Prev() { + MOZ_ASSERT(mCurrent); + + nsIContent* parent; + nsIContent* last; + if (mCurrent == mOwner) { + // Get last child of mOwner + StyleChildrenIterator ownerIter(mOwner, false /* aStartAtBeginning */); + last = ownerIter.GetPreviousChild(); + + parent = last; + } else { + // Create parent's iterator and move to mCurrent + parent = mCurrent->GetFlattenedTreeParent(); + StyleChildrenIterator parentIter(parent); + parentIter.Seek(mCurrent); + + // Get previous sibling + last = parentIter.GetPreviousChild(); + } + + while (last) { + parent = last; + if (IsHostOrSlot(parent)) { + // Skip contents in other scopes + break; + } + + // Find last child + StyleChildrenIterator iter(parent, false /* aStartAtBeginning */); + last = iter.GetPreviousChild(); + } + + // If parent is mOwner and no previous sibling remains, END traversal + SetCurrent(parent == mOwner ? nullptr : parent); +} + +static bool IsOpenPopoverWithInvoker(nsIContent* aContent) { + if (auto* popover = Element::FromNode(aContent)) { + return popover && popover->IsPopoverOpen() && + popover->GetPopoverData()->GetInvoker(); + } + return false; +} + +static nsIContent* InvokerForPopoverShowingState(nsIContent* aContent) { + Element* invoker = Element::FromNode(aContent); + if (!invoker) { + return nullptr; + } + + nsGenericHTMLElement* popover = invoker->GetEffectivePopoverTargetElement(); + if (popover && popover->IsPopoverOpen() && + popover->GetPopoverData()->GetInvoker() == invoker) { + return aContent; + } + + return nullptr; +} + +/** + * Returns scope owner of aContent. + * A scope owner is either a shadow host, or slot. + */ +static nsIContent* FindScopeOwner(nsIContent* aContent) { + nsIContent* currentContent = aContent; + while (currentContent) { + nsIContent* parent = currentContent->GetFlattenedTreeParent(); + + // Shadow host / Slot + if (IsHostOrSlot(parent)) { + return parent; + } + + currentContent = parent; + } + + return nullptr; +} + +/** + * Host and Slot elements need to be handled as if they had tabindex 0 even + * when they don't have the attribute. This is a helper method to get the + * right value for focus navigation. If aIsFocusable is passed, it is set to + * true if the element itself is focusable. + */ +static int32_t HostOrSlotTabIndexValue(const nsIContent* aContent, + bool* aIsFocusable = nullptr) { + MOZ_ASSERT(IsHostOrSlot(aContent)); + + if (aIsFocusable) { + nsIFrame* frame = aContent->GetPrimaryFrame(); + *aIsFocusable = frame && frame->IsFocusable().mTabIndex >= 0; + } + + const nsAttrValue* attrVal = + aContent->AsElement()->GetParsedAttr(nsGkAtoms::tabindex); + if (!attrVal) { + return 0; + } + + if (attrVal->Type() == nsAttrValue::eInteger) { + return attrVal->GetIntegerValue(); + } + + return -1; +} + +nsIContent* nsFocusManager::GetNextTabbableContentInScope( + nsIContent* aOwner, nsIContent* aStartContent, + nsIContent* aOriginalStartContent, bool aForward, int32_t aCurrentTabIndex, + bool aIgnoreTabIndex, bool aForDocumentNavigation, bool aNavigateByKey, + bool aSkipOwner) { + MOZ_ASSERT( + IsHostOrSlot(aOwner) || IsOpenPopoverWithInvoker(aOwner), + "Scope owner should be host, slot or an open popover with invoker set."); + + // XXX: Why don't we ignore tabindex when the current tabindex < 0? + MOZ_ASSERT_IF(aCurrentTabIndex < 0, aIgnoreTabIndex); + + if (!aSkipOwner && (aForward && aOwner == aStartContent)) { + if (nsIFrame* frame = aOwner->GetPrimaryFrame()) { + auto focusable = frame->IsFocusable(); + if (focusable && focusable.mTabIndex >= 0) { + return aOwner; + } + } + } + + // + // Iterate contents in scope + // + ScopedContentTraversal contentTraversal(aStartContent, aOwner); + nsCOMPtr<nsIContent> iterContent; + nsIContent* firstNonChromeOnly = + aStartContent->IsInNativeAnonymousSubtree() + ? aStartContent->FindFirstNonChromeOnlyAccessContent() + : nullptr; + while (1) { + // Iterate tab index to find corresponding contents in scope + + while (1) { + // Iterate remaining contents in scope to find next content to focus + + // Get next content + aForward ? contentTraversal.Next() : contentTraversal.Prev(); + iterContent = contentTraversal.GetCurrent(); + + if (firstNonChromeOnly && firstNonChromeOnly == iterContent) { + // We just broke out from the native anonymous content, so move + // to the previous/next node of the native anonymous owner. + if (aForward) { + contentTraversal.Next(); + } else { + contentTraversal.Prev(); + } + iterContent = contentTraversal.GetCurrent(); + } + if (!iterContent) { + // Reach the end + break; + } + + int32_t tabIndex = 0; + if (iterContent->IsInNativeAnonymousSubtree() && + iterContent->GetPrimaryFrame()) { + tabIndex = iterContent->GetPrimaryFrame()->IsFocusable().mTabIndex; + } else if (IsHostOrSlot(iterContent)) { + tabIndex = HostOrSlotTabIndexValue(iterContent); + } else { + nsIFrame* frame = iterContent->GetPrimaryFrame(); + if (!frame) { + continue; + } + tabIndex = frame->IsFocusable().mTabIndex; + } + if (tabIndex < 0 || !(aIgnoreTabIndex || tabIndex == aCurrentTabIndex)) { + continue; + } + + if (!IsHostOrSlot(iterContent)) { + nsCOMPtr<nsIContent> elementInFrame; + bool checkSubDocument = true; + if (aForDocumentNavigation && + TryDocumentNavigation(iterContent, &checkSubDocument, + getter_AddRefs(elementInFrame))) { + return elementInFrame; + } + if (!checkSubDocument) { + continue; + } + + if (TryToMoveFocusToSubDocument(iterContent, aOriginalStartContent, + aForward, aForDocumentNavigation, + aNavigateByKey, + getter_AddRefs(elementInFrame))) { + return elementInFrame; + } + + // Found content to focus + return iterContent; + } + + // Search in scope owned by iterContent + nsIContent* contentToFocus = GetNextTabbableContentInScope( + iterContent, iterContent, aOriginalStartContent, aForward, + aForward ? 1 : 0, aIgnoreTabIndex, aForDocumentNavigation, + aNavigateByKey, false /* aSkipOwner */); + if (contentToFocus) { + return contentToFocus; + } + }; + + // If already at lowest priority tab (0), end search completely. + // A bit counterintuitive but true, tabindex order goes 1, 2, ... 32767, 0 + if (aCurrentTabIndex == (aForward ? 0 : 1)) { + break; + } + + // We've been just trying to find some focusable element, and haven't, so + // bail out. + if (aIgnoreTabIndex) { + break; + } + + // Continue looking for next highest priority tabindex + aCurrentTabIndex = GetNextTabIndex(aOwner, aCurrentTabIndex, aForward); + contentTraversal.Reset(); + } + + // Return scope owner at last for backward navigation if its tabindex + // is non-negative + if (!aSkipOwner && !aForward) { + if (nsIFrame* frame = aOwner->GetPrimaryFrame()) { + auto focusable = frame->IsFocusable(); + if (focusable && focusable.mTabIndex >= 0) { + return aOwner; + } + } + } + + return nullptr; +} + +nsIContent* nsFocusManager::GetNextTabbableContentInAncestorScopes( + nsIContent* aStartOwner, nsCOMPtr<nsIContent>& aStartContent /* inout */, + nsIContent* aOriginalStartContent, bool aForward, int32_t* aCurrentTabIndex, + bool* aIgnoreTabIndex, bool aForDocumentNavigation, bool aNavigateByKey) { + MOZ_ASSERT(aStartOwner == FindScopeOwner(aStartContent), + "aStartOWner should be the scope owner of aStartContent"); + MOZ_ASSERT(IsHostOrSlot(aStartOwner), "scope owner should be host or slot"); + + nsCOMPtr<nsIContent> owner = aStartOwner; + nsCOMPtr<nsIContent> startContent = aStartContent; + while (IsHostOrSlot(owner)) { + int32_t tabIndex = 0; + if (IsHostOrSlot(startContent)) { + tabIndex = HostOrSlotTabIndexValue(startContent); + } else if (nsIFrame* frame = startContent->GetPrimaryFrame()) { + tabIndex = frame->IsFocusable().mTabIndex; + } else { + tabIndex = startContent->IsFocusableWithoutStyle().mTabIndex; + } + nsIContent* contentToFocus = GetNextTabbableContentInScope( + owner, startContent, aOriginalStartContent, aForward, tabIndex, + tabIndex < 0, aForDocumentNavigation, aNavigateByKey, + false /* aSkipOwner */); + if (contentToFocus) { + return contentToFocus; + } + + startContent = owner; + owner = FindScopeOwner(startContent); + } + + // If not found in shadow DOM, search from the top level shadow host in light + // DOM + aStartContent = startContent; + *aCurrentTabIndex = HostOrSlotTabIndexValue(startContent); + + if (*aCurrentTabIndex < 0) { + *aIgnoreTabIndex = true; + } + + return nullptr; +} + +static nsIContent* GetTopLevelScopeOwner(nsIContent* aContent) { + nsIContent* topLevelScopeOwner = nullptr; + while (aContent) { + if (HTMLSlotElement* slot = aContent->GetAssignedSlot()) { + aContent = slot; + topLevelScopeOwner = aContent; + } else if (ShadowRoot* shadowRoot = aContent->GetContainingShadow()) { + aContent = shadowRoot->Host(); + topLevelScopeOwner = aContent; + } else { + aContent = aContent->GetParent(); + if (aContent && (HTMLSlotElement::FromNode(aContent) || + IsOpenPopoverWithInvoker(aContent))) { + topLevelScopeOwner = aContent; + } + } + } + + return topLevelScopeOwner; +} + +nsresult nsFocusManager::GetNextTabbableContent( + PresShell* aPresShell, nsIContent* aRootContent, + nsIContent* aOriginalStartContent, nsIContent* aStartContent, bool aForward, + int32_t aCurrentTabIndex, bool aIgnoreTabIndex, bool aForDocumentNavigation, + bool aNavigateByKey, bool aSkipPopover, nsIContent** aResultContent) { + *aResultContent = nullptr; + + if (!aStartContent) { + return NS_OK; + } + + nsCOMPtr<nsIContent> startContent = aStartContent; + nsCOMPtr<nsIContent> currentTopLevelScopeOwner = + GetTopLevelScopeOwner(startContent); + + LOGCONTENTNAVIGATION("GetNextTabbable: %s", startContent); + LOGFOCUSNAVIGATION((" tabindex: %d", aCurrentTabIndex)); + + // If startContent is a shadow host or slot in forward navigation, + // search in scope owned by startContent + if (aForward && IsHostOrSlot(startContent)) { + nsIContent* contentToFocus = GetNextTabbableContentInScope( + startContent, startContent, aOriginalStartContent, aForward, 1, + aIgnoreTabIndex, aForDocumentNavigation, aNavigateByKey, + true /* aSkipOwner */); + if (contentToFocus) { + NS_ADDREF(*aResultContent = contentToFocus); + return NS_OK; + } + } + + // If startContent is a popover invoker, search the popover scope. + if (!aSkipPopover) { + if (InvokerForPopoverShowingState(startContent)) { + if (aForward) { + RefPtr<nsIContent> popover = + startContent->GetEffectivePopoverTargetElement(); + nsIContent* contentToFocus = GetNextTabbableContentInScope( + popover, popover, aOriginalStartContent, aForward, 1, + aIgnoreTabIndex, aForDocumentNavigation, aNavigateByKey, + true /* aSkipOwner */); + if (contentToFocus) { + NS_ADDREF(*aResultContent = contentToFocus); + return NS_OK; + } + } + } + } + + // If startContent is in a scope owned by Shadow DOM search from scope + // including startContent + if (nsCOMPtr<nsIContent> owner = FindScopeOwner(startContent)) { + nsIContent* contentToFocus = GetNextTabbableContentInAncestorScopes( + owner, startContent /* inout */, aOriginalStartContent, aForward, + &aCurrentTabIndex, &aIgnoreTabIndex, aForDocumentNavigation, + aNavigateByKey); + if (contentToFocus) { + NS_ADDREF(*aResultContent = contentToFocus); + return NS_OK; + } + } + + // If we reach here, it means no next tabbable content in shadow DOM. + // We need to continue searching in light DOM, starting at the top level + // shadow host in light DOM (updated startContent) and its tabindex + // (updated aCurrentTabIndex). + MOZ_ASSERT(!FindScopeOwner(startContent), + "startContent should not be owned by Shadow DOM at this point"); + + nsPresContext* presContext = aPresShell->GetPresContext(); + + bool getNextFrame = true; + nsCOMPtr<nsIContent> iterStartContent = startContent; + nsIContent* topLevelScopeStartContent = startContent; + // Iterate tab index to find corresponding contents + while (1) { + nsIFrame* frame = iterStartContent->GetPrimaryFrame(); + // if there is no frame, look for another content node that has a frame + while (!frame) { + // if the root content doesn't have a frame, just return + if (iterStartContent == aRootContent) { + return NS_OK; + } + + // look for the next or previous content node in tree order + iterStartContent = aForward ? iterStartContent->GetNextNode() + : iterStartContent->GetPrevNode(); + if (!iterStartContent) { + break; + } + + frame = iterStartContent->GetPrimaryFrame(); + // Host without frame, enter its scope. + if (!frame && iterStartContent->GetShadowRoot()) { + int32_t tabIndex = HostOrSlotTabIndexValue(iterStartContent); + if (tabIndex >= 0 && + (aIgnoreTabIndex || aCurrentTabIndex == tabIndex)) { + nsIContent* contentToFocus = GetNextTabbableContentInScope( + iterStartContent, iterStartContent, aOriginalStartContent, + aForward, aForward ? 1 : 0, aIgnoreTabIndex, + aForDocumentNavigation, aNavigateByKey, true /* aSkipOwner */); + if (contentToFocus) { + NS_ADDREF(*aResultContent = contentToFocus); + return NS_OK; + } + } + } + // we've already skipped over the initial focused content, so we + // don't want to traverse frames. + getNextFrame = false; + } + + Maybe<nsFrameIterator> frameIterator; + if (frame) { + // For tab navigation, pass false for aSkipPopupChecks so that we don't + // iterate into or out of a popup. For document naviation pass true to + // ignore these boundaries. + frameIterator.emplace(presContext, frame, nsFrameIterator::Type::PreOrder, + false, // aVisual + false, // aLockInScrollView + true, // aFollowOOFs + aForDocumentNavigation // aSkipPopupChecks + ); + MOZ_ASSERT(frameIterator); + + if (iterStartContent == aRootContent) { + if (!aForward) { + frameIterator->Last(); + } else if (aRootContent->IsFocusableWithoutStyle()) { + frameIterator->Next(); + } + frame = frameIterator->CurrentItem(); + } else if (getNextFrame && + (!iterStartContent || + !iterStartContent->IsHTMLElement(nsGkAtoms::area))) { + // Need to do special check in case we're in an imagemap which has + // multiple content nodes per frame, so don't skip over the starting + // frame. + frame = frameIterator->Traverse(aForward); + } + } + + nsIContent* oldTopLevelScopeOwner = nullptr; + // Walk frames to find something tabbable matching aCurrentTabIndex + while (frame) { + // Try to find the topmost scope owner, since we want to skip the node + // that is not owned by document in frame traversal. + const nsCOMPtr<nsIContent> currentContent = frame->GetContent(); + if (currentTopLevelScopeOwner) { + oldTopLevelScopeOwner = currentTopLevelScopeOwner; + } + currentTopLevelScopeOwner = GetTopLevelScopeOwner(currentContent); + + // We handle popover case separately. + if (currentTopLevelScopeOwner && + currentTopLevelScopeOwner == oldTopLevelScopeOwner && + !IsOpenPopoverWithInvoker(currentTopLevelScopeOwner)) { + // We're within non-document scope, continue. + do { + if (aForward) { + frameIterator->Next(); + } else { + frameIterator->Prev(); + } + frame = frameIterator->CurrentItem(); + // For the usage of GetPrevContinuation, see the comment + // at the end of while (frame) loop. + } while (frame && frame->GetPrevContinuation()); + continue; + } + + // Stepping out popover scope. + // For forward, search for the next tabbable content after invoker. + // For backward, we should get back to the invoker. + if (oldTopLevelScopeOwner && + IsOpenPopoverWithInvoker(oldTopLevelScopeOwner) && + currentTopLevelScopeOwner != oldTopLevelScopeOwner) { + if (auto* popover = Element::FromNode(oldTopLevelScopeOwner)) { + RefPtr<nsIContent> invokerContent = + popover->GetPopoverData()->GetInvoker()->AsContent(); + if (aForward) { + nsIFrame* frame = invokerContent->GetPrimaryFrame(); + int32_t tabIndex = frame->IsFocusable().mTabIndex; + RefPtr<nsIContent> rootElement = invokerContent; + if (auto* doc = invokerContent->GetComposedDoc()) { + rootElement = doc->GetRootElement(); + } + if (tabIndex >= 0 && + (aIgnoreTabIndex || aCurrentTabIndex == tabIndex)) { + nsresult rv = GetNextTabbableContent( + aPresShell, rootElement, nullptr, invokerContent, true, + tabIndex, false, false, aNavigateByKey, true, aResultContent); + if (NS_SUCCEEDED(rv) && *aResultContent) { + return rv; + } + } + } else if (invokerContent && + invokerContent->IsFocusableWithoutStyle()) { + // FIXME(emilio): The check above should probably use + // nsIFrame::IsFocusable, not IsFocusableWithoutStyle. + invokerContent.forget(aResultContent); + return NS_OK; + } + } + } + + if (!aForward) { + if (InvokerForPopoverShowingState(currentContent)) { + RefPtr<nsIContent> popover = + currentContent->GetEffectivePopoverTargetElement(); + nsIContent* contentToFocus = GetNextTabbableContentInScope( + popover, popover, aOriginalStartContent, aForward, 0, + aIgnoreTabIndex, aForDocumentNavigation, aNavigateByKey, + true /* aSkipOwner */); + + if (contentToFocus) { + NS_ADDREF(*aResultContent = contentToFocus); + return NS_OK; + } + } + } + // For document navigation, check if this element is an open panel. Since + // panels aren't focusable (tabIndex would be -1), we'll just assume that + // for document navigation, the tabIndex is 0. + if (aForDocumentNavigation && currentContent && (aCurrentTabIndex == 0) && + currentContent->IsXULElement(nsGkAtoms::panel)) { + nsMenuPopupFrame* popupFrame = do_QueryFrame(frame); + // Check if the panel is open. Closed panels are ignored since you can't + // focus anything in them. + if (popupFrame && popupFrame->IsOpen()) { + // When moving backward, skip the popup we started in otherwise it + // will be selected again. + bool validPopup = true; + if (!aForward) { + nsIContent* content = topLevelScopeStartContent; + while (content) { + if (content == currentContent) { + validPopup = false; + break; + } + + content = content->GetParent(); + } + } + + if (validPopup) { + // Since a panel isn't focusable itself, find the first focusable + // content within the popup. If there isn't any focusable content + // in the popup, skip this popup and continue iterating through the + // frames. We pass the panel itself (currentContent) as the starting + // and root content, so that we only find content within the panel. + // Note also that we pass false for aForDocumentNavigation since we + // want to locate the first content, not the first document. + nsresult rv = GetNextTabbableContent( + aPresShell, currentContent, nullptr, currentContent, true, 1, + false, false, aNavigateByKey, false, aResultContent); + if (NS_SUCCEEDED(rv) && *aResultContent) { + return rv; + } + } + } + } + + // As of now, 2018/04/12, sequential focus navigation is still + // in the obsolete Shadow DOM specification. + // http://w3c.github.io/webcomponents/spec/shadow/#sequential-focus-navigation + // "if ELEMENT is focusable, a shadow host, or a slot element, + // append ELEMENT to NAVIGATION-ORDER." + // and later in "For each element ELEMENT in NAVIGATION-ORDER: " + // hosts and slots are handled before other elements. + if (currentTopLevelScopeOwner && + !IsOpenPopoverWithInvoker(currentTopLevelScopeOwner)) { + bool focusableHostSlot; + int32_t tabIndex = HostOrSlotTabIndexValue(currentTopLevelScopeOwner, + &focusableHostSlot); + // Host or slot itself isn't focusable or going backwards, enter its + // scope. + if ((!aForward || !focusableHostSlot) && tabIndex >= 0 && + (aIgnoreTabIndex || aCurrentTabIndex == tabIndex)) { + nsIContent* contentToFocus = GetNextTabbableContentInScope( + currentTopLevelScopeOwner, currentTopLevelScopeOwner, + aOriginalStartContent, aForward, aForward ? 1 : 0, + aIgnoreTabIndex, aForDocumentNavigation, aNavigateByKey, + true /* aSkipOwner */); + if (contentToFocus) { + NS_ADDREF(*aResultContent = contentToFocus); + return NS_OK; + } + // If we've wrapped around already, then carry on. + if (aOriginalStartContent && + currentTopLevelScopeOwner == + GetTopLevelScopeOwner(aOriginalStartContent)) { + // FIXME: Shouldn't this return null instead? aOriginalStartContent + // isn't focusable after all. + NS_ADDREF(*aResultContent = aOriginalStartContent); + return NS_OK; + } + } + // There is no next tabbable content in currentTopLevelScopeOwner's + // scope. We should continue the loop in order to skip all contents that + // is in currentTopLevelScopeOwner's scope. + continue; + } + + MOZ_ASSERT( + !GetTopLevelScopeOwner(currentContent) || + IsOpenPopoverWithInvoker(GetTopLevelScopeOwner(currentContent)), + "currentContent should be in top-level-scope at this point unless " + "for popover case"); + + // TabIndex not set defaults to 0 for form elements, anchors and other + // elements that are normally focusable. Tabindex defaults to -1 + // for elements that are not normally focusable. + // The returned computed tabindex from IsFocusable() is as follows: + // clang-format off + // < 0 not tabbable at all + // == 0 in normal tab order (last after positive tabindexed items) + // > 0 can be tabbed to in the order specified by this value + // clang-format on + int32_t tabIndex = frame->IsFocusable().mTabIndex; + + LOGCONTENTNAVIGATION("Next Tabbable %s:", frame->GetContent()); + LOGFOCUSNAVIGATION( + (" with tabindex: %d expected: %d", tabIndex, aCurrentTabIndex)); + + if (tabIndex >= 0) { + NS_ASSERTION(currentContent, + "IsFocusable set a tabindex for a frame with no content"); + if (!aForDocumentNavigation && + currentContent->IsHTMLElement(nsGkAtoms::img) && + currentContent->AsElement()->HasAttr(nsGkAtoms::usemap)) { + // This is an image with a map. Image map areas are not traversed by + // nsFrameIterator so look for the next or previous area element. + nsIContent* areaContent = GetNextTabbableMapArea( + aForward, aCurrentTabIndex, currentContent->AsElement(), + iterStartContent); + if (areaContent) { + NS_ADDREF(*aResultContent = areaContent); + return NS_OK; + } + } else if (aIgnoreTabIndex || aCurrentTabIndex == tabIndex) { + // break out if we've wrapped around to the start again. + if (aOriginalStartContent && + currentContent == aOriginalStartContent) { + NS_ADDREF(*aResultContent = currentContent); + return NS_OK; + } + + // If this is a remote child browser, call NavigateDocument to have + // the child process continue the navigation. Return a special error + // code to have the caller return early. If the child ends up not + // being focusable in some way, the child process will call back + // into document navigation again by calling MoveFocus. + if (BrowserParent* remote = BrowserParent::GetFrom(currentContent)) { + if (aNavigateByKey) { + remote->NavigateByKey(aForward, aForDocumentNavigation); + return NS_SUCCESS_DOM_NO_OPERATION; + } + return NS_OK; + } + + // Same as above but for out-of-process iframes + if (auto* bbc = BrowserBridgeChild::GetFrom(currentContent)) { + if (aNavigateByKey) { + bbc->NavigateByKey(aForward, aForDocumentNavigation); + return NS_SUCCESS_DOM_NO_OPERATION; + } + return NS_OK; + } + + // Next, for document navigation, check if this a non-remote child + // document. + bool checkSubDocument = true; + if (aForDocumentNavigation && + TryDocumentNavigation(currentContent, &checkSubDocument, + aResultContent)) { + return NS_OK; + } + + if (checkSubDocument) { + // found a node with a matching tab index. Check if it is a child + // frame. If so, navigate into the child frame instead. + if (TryToMoveFocusToSubDocument( + currentContent, aOriginalStartContent, aForward, + aForDocumentNavigation, aNavigateByKey, aResultContent)) { + MOZ_ASSERT(*aResultContent); + return NS_OK; + } + // otherwise, use this as the next content node to tab to, unless + // this was the element we started on. This would happen for + // instance on an element with child frames, where frame navigation + // could return the original element again. In that case, just skip + // it. Also, if the next content node is the root content, then + // return it. This latter case would happen only if someone made a + // popup focusable. + else if (currentContent == aRootContent || + currentContent != startContent) { + NS_ADDREF(*aResultContent = currentContent); + return NS_OK; + } + } + } + } else if (aOriginalStartContent && + currentContent == aOriginalStartContent) { + // not focusable, so return if we have wrapped around to the original + // content. This is necessary in case the original starting content was + // not focusable. + // + // FIXME: Shouldn't this return null instead? currentContent isn't + // focusable after all. + NS_ADDREF(*aResultContent = currentContent); + return NS_OK; + } + + // Move to the next or previous frame, but ignore continuation frames + // since only the first frame should be involved in focusability. + // Otherwise, a loop will occur in the following example: + // <span tabindex="1">...<a/><a/>...</span> + // where the text wraps onto multiple lines. Tabbing from the second + // link can find one of the span's continuation frames between the link + // and the end of the span, and the span would end up getting focused + // again. + do { + if (aForward) { + frameIterator->Next(); + } else { + frameIterator->Prev(); + } + frame = frameIterator->CurrentItem(); + } while (frame && frame->GetPrevContinuation()); + } + + // If already at lowest priority tab (0), end search completely. + // A bit counterintuitive but true, tabindex order goes 1, 2, ... 32767, 0 + if (aCurrentTabIndex == (aForward ? 0 : 1)) { + // if going backwards, the canvas should be focused once the beginning + // has been reached, so get the root element. + if (!aForward) { + nsCOMPtr<nsPIDOMWindowOuter> window = GetCurrentWindow(aRootContent); + NS_ENSURE_TRUE(window, NS_ERROR_FAILURE); + + RefPtr<Element> docRoot = GetRootForFocus( + window, aRootContent->GetComposedDoc(), false, true); + FocusFirst(docRoot, aResultContent); + } + break; + } + + // continue looking for next highest priority tabindex + aCurrentTabIndex = + GetNextTabIndex(aRootContent, aCurrentTabIndex, aForward); + startContent = iterStartContent = aRootContent; + currentTopLevelScopeOwner = GetTopLevelScopeOwner(startContent); + } + + return NS_OK; +} + +bool nsFocusManager::TryDocumentNavigation(nsIContent* aCurrentContent, + bool* aCheckSubDocument, + nsIContent** aResultContent) { + *aCheckSubDocument = true; + if (RefPtr<Element> rootElementForChildDocument = + GetRootForChildDocument(aCurrentContent)) { + // If GetRootForChildDocument returned something then call + // FocusFirst to find the root or first element to focus within + // the child document. If this is a frameset though, skip this and + // fall through to normal tab navigation to iterate into + // the frameset's frames and locate the first focusable frame. + if (!rootElementForChildDocument->IsHTMLElement(nsGkAtoms::frameset)) { + *aCheckSubDocument = false; + Unused << FocusFirst(rootElementForChildDocument, aResultContent); + return *aResultContent != nullptr; + } + } else { + // Set aCheckSubDocument to false, as this was neither a frame + // type element or a child document that was focusable. + *aCheckSubDocument = false; + } + + return false; +} + +bool nsFocusManager::TryToMoveFocusToSubDocument( + nsIContent* aCurrentContent, nsIContent* aOriginalStartContent, + bool aForward, bool aForDocumentNavigation, bool aNavigateByKey, + nsIContent** aResultContent) { + Document* doc = aCurrentContent->GetComposedDoc(); + NS_ASSERTION(doc, "content not in document"); + Document* subdoc = doc->GetSubDocumentFor(aCurrentContent); + if (subdoc && !subdoc->EventHandlingSuppressed()) { + if (aForward) { + // When tabbing forward into a frame, return the root + // frame so that the canvas becomes focused. + if (nsCOMPtr<nsPIDOMWindowOuter> subframe = subdoc->GetWindow()) { + *aResultContent = GetRootForFocus(subframe, subdoc, false, true); + if (*aResultContent) { + NS_ADDREF(*aResultContent); + return true; + } + } + } + if (RefPtr<Element> rootElement = subdoc->GetRootElement()) { + if (RefPtr<PresShell> subPresShell = subdoc->GetPresShell()) { + nsresult rv = GetNextTabbableContent( + subPresShell, rootElement, aOriginalStartContent, rootElement, + aForward, (aForward ? 1 : 0), false, aForDocumentNavigation, + aNavigateByKey, false, aResultContent); + NS_ENSURE_SUCCESS(rv, false); + if (*aResultContent) { + return true; + } + } + } + } + return false; +} + +nsIContent* nsFocusManager::GetNextTabbableMapArea(bool aForward, + int32_t aCurrentTabIndex, + Element* aImageContent, + nsIContent* aStartContent) { + if (aImageContent->IsInComposedDoc()) { + HTMLImageElement* imgElement = HTMLImageElement::FromNode(aImageContent); + // The caller should check the element type, so we can assert here. + MOZ_ASSERT(imgElement); + + nsCOMPtr<nsIContent> mapContent = imgElement->FindImageMap(); + if (!mapContent) { + return nullptr; + } + // First see if the the start content is in this map + Maybe<uint32_t> indexOfStartContent = + mapContent->ComputeIndexOf(aStartContent); + nsIContent* scanStartContent; + Focusable focusable; + if (indexOfStartContent.isNothing() || + ((focusable = aStartContent->IsFocusableWithoutStyle()) && + focusable.mTabIndex != aCurrentTabIndex)) { + // If aStartContent is in this map we must start iterating past it. + // We skip the case where aStartContent has tabindex == aStartContent + // since the next tab ordered element might be before it + // (or after for backwards) in the child list. + scanStartContent = + aForward ? mapContent->GetFirstChild() : mapContent->GetLastChild(); + } else { + scanStartContent = aForward ? aStartContent->GetNextSibling() + : aStartContent->GetPreviousSibling(); + } + + for (nsCOMPtr<nsIContent> areaContent = scanStartContent; areaContent; + areaContent = aForward ? areaContent->GetNextSibling() + : areaContent->GetPreviousSibling()) { + focusable = areaContent->IsFocusableWithoutStyle(); + if (focusable && focusable.mTabIndex == aCurrentTabIndex) { + return areaContent; + } + } + } + + return nullptr; +} + +int32_t nsFocusManager::GetNextTabIndex(nsIContent* aParent, + int32_t aCurrentTabIndex, + bool aForward) { + int32_t tabIndex, childTabIndex; + StyleChildrenIterator iter(aParent); + + if (aForward) { + tabIndex = 0; + for (nsIContent* child = iter.GetNextChild(); child; + child = iter.GetNextChild()) { + // Skip child's descendants if child is a shadow host or slot, as they are + // in the focus navigation scope owned by child's shadow root + if (!IsHostOrSlot(child)) { + childTabIndex = GetNextTabIndex(child, aCurrentTabIndex, aForward); + if (childTabIndex > aCurrentTabIndex && childTabIndex != tabIndex) { + tabIndex = (tabIndex == 0 || childTabIndex < tabIndex) ? childTabIndex + : tabIndex; + } + } + + nsAutoString tabIndexStr; + if (child->IsElement()) { + child->AsElement()->GetAttr(nsGkAtoms::tabindex, tabIndexStr); + } + nsresult ec; + int32_t val = tabIndexStr.ToInteger(&ec); + if (NS_SUCCEEDED(ec) && val > aCurrentTabIndex && val != tabIndex) { + tabIndex = (tabIndex == 0 || val < tabIndex) ? val : tabIndex; + } + } + } else { /* !aForward */ + tabIndex = 1; + for (nsIContent* child = iter.GetNextChild(); child; + child = iter.GetNextChild()) { + // Skip child's descendants if child is a shadow host or slot, as they are + // in the focus navigation scope owned by child's shadow root + if (!IsHostOrSlot(child)) { + childTabIndex = GetNextTabIndex(child, aCurrentTabIndex, aForward); + if ((aCurrentTabIndex == 0 && childTabIndex > tabIndex) || + (childTabIndex < aCurrentTabIndex && childTabIndex > tabIndex)) { + tabIndex = childTabIndex; + } + } + + nsAutoString tabIndexStr; + if (child->IsElement()) { + child->AsElement()->GetAttr(nsGkAtoms::tabindex, tabIndexStr); + } + nsresult ec; + int32_t val = tabIndexStr.ToInteger(&ec); + if (NS_SUCCEEDED(ec)) { + if ((aCurrentTabIndex == 0 && val > tabIndex) || + (val < aCurrentTabIndex && val > tabIndex)) { + tabIndex = val; + } + } + } + } + + return tabIndex; +} + +nsresult nsFocusManager::FocusFirst(Element* aRootElement, + nsIContent** aNextContent) { + if (!aRootElement) { + return NS_OK; + } + + Document* doc = aRootElement->GetComposedDoc(); + if (doc) { + if (nsContentUtils::IsChromeDoc(doc)) { + // If the redirectdocumentfocus attribute is set, redirect the focus to a + // specific element. This is primarily used to retarget the focus to the + // urlbar during document navigation. + nsAutoString retarget; + + if (aRootElement->GetAttr(nsGkAtoms::retargetdocumentfocus, retarget)) { + RefPtr<Element> element = doc->GetElementById(retarget); + nsCOMPtr<nsIContent> retargetElement = + FlushAndCheckIfFocusable(element, 0); + if (retargetElement) { + retargetElement.forget(aNextContent); + return NS_OK; + } + } + } + + nsCOMPtr<nsIDocShell> docShell = doc->GetDocShell(); + if (docShell->ItemType() == nsIDocShellTreeItem::typeChrome) { + // If the found content is in a chrome shell, navigate forward one + // tabbable item so that the first item is focused. Note that we + // always go forward and not back here. + if (RefPtr<PresShell> presShell = doc->GetPresShell()) { + return GetNextTabbableContent(presShell, aRootElement, nullptr, + aRootElement, true, 1, false, false, true, + false, aNextContent); + } + } + } + + NS_ADDREF(*aNextContent = aRootElement); + return NS_OK; +} + +Element* nsFocusManager::GetRootForFocus(nsPIDOMWindowOuter* aWindow, + Document* aDocument, + bool aForDocumentNavigation, + bool aCheckVisibility) { + if (!aForDocumentNavigation) { + nsCOMPtr<nsIDocShell> docShell = aWindow->GetDocShell(); + if (docShell->ItemType() == nsIDocShellTreeItem::typeChrome) { + return nullptr; + } + } + + if (aCheckVisibility && !IsWindowVisible(aWindow)) return nullptr; + + // If the body is contenteditable, use the editor's root element rather than + // the actual root element. + RefPtr<Element> rootElement = + nsLayoutUtils::GetEditableRootContentByContentEditable(aDocument); + if (!rootElement || !rootElement->GetPrimaryFrame()) { + rootElement = aDocument->GetRootElement(); + if (!rootElement) { + return nullptr; + } + } + + if (aCheckVisibility && !rootElement->GetPrimaryFrame()) { + return nullptr; + } + + // Finally, check if this is a frameset + if (aDocument && aDocument->IsHTMLOrXHTML()) { + Element* htmlChild = aDocument->GetHtmlChildElement(nsGkAtoms::frameset); + if (htmlChild) { + // In document navigation mode, return the frameset so that navigation + // descends into the child frames. + return aForDocumentNavigation ? htmlChild : nullptr; + } + } + + return rootElement; +} + +Element* nsFocusManager::GetRootForChildDocument(nsIContent* aContent) { + // Check for elements that represent child documents, that is, browsers, + // editors or frames from a frameset. We don't include iframes since we + // consider them to be an integral part of the same window or page. + if (!aContent || !(aContent->IsXULElement(nsGkAtoms::browser) || + aContent->IsXULElement(nsGkAtoms::editor) || + aContent->IsHTMLElement(nsGkAtoms::frame))) { + return nullptr; + } + + Document* doc = aContent->GetComposedDoc(); + if (!doc) { + return nullptr; + } + + Document* subdoc = doc->GetSubDocumentFor(aContent); + if (!subdoc || subdoc->EventHandlingSuppressed()) { + return nullptr; + } + + nsCOMPtr<nsPIDOMWindowOuter> window = subdoc->GetWindow(); + return GetRootForFocus(window, subdoc, true, true); +} + +static bool IsLink(nsIContent* aContent) { + return aContent->IsElement() && aContent->AsElement()->IsLink(); +} + +void nsFocusManager::GetFocusInSelection(nsPIDOMWindowOuter* aWindow, + nsIContent* aStartSelection, + nsIContent* aEndSelection, + nsIContent** aFocusedContent) { + *aFocusedContent = nullptr; + + nsCOMPtr<nsIContent> testContent = aStartSelection; + nsCOMPtr<nsIContent> nextTestContent = aEndSelection; + + nsCOMPtr<nsIContent> currentFocus = aWindow->GetFocusedElement(); + + // We now have the correct start node in selectionContent! + // Search for focusable elements, starting with selectionContent + + // Method #1: Keep going up while we look - an ancestor might be focusable + // We could end the loop earlier, such as when we're no longer + // in the same frame, by comparing selectionContent->GetPrimaryFrame() + // with a variable holding the starting selectionContent + while (testContent) { + // Keep testing while selectionContent is equal to something, + // eventually we'll run out of ancestors + + if (testContent == currentFocus || IsLink(testContent)) { + testContent.forget(aFocusedContent); + return; + } + + // Get the parent + testContent = testContent->GetParent(); + + if (!testContent) { + // We run this loop again, checking the ancestor chain of the selection's + // end point + testContent = nextTestContent; + nextTestContent = nullptr; + } + } + + // We couldn't find an anchor that was an ancestor of the selection start + // Method #2: look for anchor in selection's primary range (depth first + // search) + + nsCOMPtr<nsIContent> selectionNode = aStartSelection; + nsCOMPtr<nsIContent> endSelectionNode = aEndSelection; + nsCOMPtr<nsIContent> testNode; + + do { + testContent = selectionNode; + + // We're looking for any focusable link that could be part of the + // main document's selection. + if (testContent == currentFocus || IsLink(testContent)) { + testContent.forget(aFocusedContent); + return; + } + + nsIContent* testNode = selectionNode->GetFirstChild(); + if (testNode) { + selectionNode = testNode; + continue; + } + + if (selectionNode == endSelectionNode) { + break; + } + testNode = selectionNode->GetNextSibling(); + if (testNode) { + selectionNode = testNode; + continue; + } + + do { + // GetParent is OK here, instead of GetParentNode, because the only case + // where the latter returns something different from the former is when + // GetParentNode is the document. But in that case we would simply get + // null for selectionNode when setting it to testNode->GetNextSibling() + // (because a document has no next sibling). And then the next iteration + // of this loop would get null for GetParentNode anyway, and break out of + // all the loops. + testNode = selectionNode->GetParent(); + if (!testNode || testNode == endSelectionNode) { + selectionNode = nullptr; + break; + } + selectionNode = testNode->GetNextSibling(); + if (selectionNode) { + break; + } + selectionNode = testNode; + } while (true); + } while (selectionNode && selectionNode != endSelectionNode); +} + +static void MaybeUnlockPointer(BrowsingContext* aCurrentFocusedContext) { + if (!PointerLockManager::IsInLockContext(aCurrentFocusedContext)) { + PointerLockManager::Unlock(); + } +} + +class PointerUnlocker : public Runnable { + public: + PointerUnlocker() : mozilla::Runnable("PointerUnlocker") { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!PointerUnlocker::sActiveUnlocker); + PointerUnlocker::sActiveUnlocker = this; + } + + ~PointerUnlocker() { + if (PointerUnlocker::sActiveUnlocker == this) { + PointerUnlocker::sActiveUnlocker = nullptr; + } + } + + NS_IMETHOD Run() override { + if (PointerUnlocker::sActiveUnlocker == this) { + PointerUnlocker::sActiveUnlocker = nullptr; + } + NS_ENSURE_STATE(nsFocusManager::GetFocusManager()); + nsPIDOMWindowOuter* focused = + nsFocusManager::GetFocusManager()->GetFocusedWindow(); + MaybeUnlockPointer(focused ? focused->GetBrowsingContext() : nullptr); + return NS_OK; + } + + static PointerUnlocker* sActiveUnlocker; +}; + +PointerUnlocker* PointerUnlocker::sActiveUnlocker = nullptr; + +void nsFocusManager::SetFocusedBrowsingContext(BrowsingContext* aContext, + uint64_t aActionId) { + if (XRE_IsParentProcess()) { + return; + } + MOZ_ASSERT(!ActionIdComparableAndLower( + aActionId, mActionIdForFocusedBrowsingContextInContent)); + mFocusedBrowsingContextInContent = aContext; + mActionIdForFocusedBrowsingContextInContent = aActionId; + if (aContext) { + // We don't send the unset but instead expect the set from + // elsewhere to take care of it. XXX Is that bad? + MOZ_ASSERT(aContext->IsInProcess()); + mozilla::dom::ContentChild* contentChild = + mozilla::dom::ContentChild::GetSingleton(); + MOZ_ASSERT(contentChild); + contentChild->SendSetFocusedBrowsingContext(aContext, aActionId); + } +} + +void nsFocusManager::SetFocusedBrowsingContextFromOtherProcess( + BrowsingContext* aContext, uint64_t aActionId) { + MOZ_ASSERT(!XRE_IsParentProcess()); + MOZ_ASSERT(aContext); + if (ActionIdComparableAndLower(aActionId, + mActionIdForFocusedBrowsingContextInContent)) { + // Unclear if this ever happens. + LOGFOCUS( + ("Ignored an attempt to set an in-process BrowsingContext [%p] as " + "focused from another process due to stale action id %" PRIu64 ".", + aContext, aActionId)); + return; + } + if (aContext->IsInProcess()) { + // This message has been in transit for long enough that + // the process association of aContext has changed since + // the other content process sent the message, because + // an iframe in that process became an out-of-process + // iframe while the IPC broadcast that we're receiving + // was in-flight. Let's just ignore this. + LOGFOCUS( + ("Ignored an attempt to set an in-process BrowsingContext [%p] as " + "focused from another process, actionid: %" PRIu64 ".", + aContext, aActionId)); + return; + } + mFocusedBrowsingContextInContent = aContext; + mActionIdForFocusedBrowsingContextInContent = aActionId; + mFocusedElement = nullptr; + mFocusedWindow = nullptr; +} + +bool nsFocusManager::SetFocusedBrowsingContextInChrome( + mozilla::dom::BrowsingContext* aContext, uint64_t aActionId) { + MOZ_ASSERT(aActionId); + if (ProcessPendingFocusedBrowsingContextActionId(aActionId)) { + MOZ_DIAGNOSTIC_ASSERT(!ActionIdComparableAndLower( + aActionId, mActionIdForFocusedBrowsingContextInChrome)); + mFocusedBrowsingContextInChrome = aContext; + mActionIdForFocusedBrowsingContextInChrome = aActionId; + return true; + } + return false; +} + +BrowsingContext* nsFocusManager::GetFocusedBrowsingContextInChrome() { + return mFocusedBrowsingContextInChrome; +} + +void nsFocusManager::BrowsingContextDetached(BrowsingContext* aContext) { + if (mFocusedBrowsingContextInChrome == aContext) { + mFocusedBrowsingContextInChrome = nullptr; + // Deliberately not adjusting the corresponding action id, because + // we don't want changes from the past to take effect. + } + if (mActiveBrowsingContextInChrome == aContext) { + mActiveBrowsingContextInChrome = nullptr; + // Deliberately not adjusting the corresponding action id, because + // we don't want changes from the past to take effect. + } +} + +void nsFocusManager::SetActiveBrowsingContextInContent( + mozilla::dom::BrowsingContext* aContext, uint64_t aActionId) { + MOZ_ASSERT(!XRE_IsParentProcess()); + MOZ_ASSERT(!aContext || aContext->IsInProcess()); + mozilla::dom::ContentChild* contentChild = + mozilla::dom::ContentChild::GetSingleton(); + MOZ_ASSERT(contentChild); + + if (ActionIdComparableAndLower(aActionId, + mActionIdForActiveBrowsingContextInContent)) { + LOGFOCUS( + ("Ignored an attempt to set an in-process BrowsingContext [%p] as " + "the active browsing context due to a stale action id %" PRIu64 ".", + aContext, aActionId)); + return; + } + + if (aContext != mActiveBrowsingContextInContent) { + if (aContext) { + contentChild->SendSetActiveBrowsingContext(aContext, aActionId); + } else if (mActiveBrowsingContextInContent) { + // We want to sync this over only if this isn't happening + // due to the active BrowsingContext switching processes, + // in which case the BrowserChild has already marked itself + // as destroying. + nsPIDOMWindowOuter* outer = + mActiveBrowsingContextInContent->GetDOMWindow(); + if (outer) { + nsPIDOMWindowInner* inner = outer->GetCurrentInnerWindow(); + if (inner) { + WindowGlobalChild* globalChild = inner->GetWindowGlobalChild(); + if (globalChild) { + RefPtr<BrowserChild> browserChild = globalChild->GetBrowserChild(); + if (browserChild && !browserChild->IsDestroyed()) { + contentChild->SendUnsetActiveBrowsingContext( + mActiveBrowsingContextInContent, aActionId); + } + } + } + } + } + } + mActiveBrowsingContextInContentSetFromOtherProcess = false; + mActiveBrowsingContextInContent = aContext; + mActionIdForActiveBrowsingContextInContent = aActionId; + MaybeUnlockPointer(aContext); +} + +void nsFocusManager::SetActiveBrowsingContextFromOtherProcess( + BrowsingContext* aContext, uint64_t aActionId) { + MOZ_ASSERT(!XRE_IsParentProcess()); + MOZ_ASSERT(aContext); + if (ActionIdComparableAndLower(aActionId, + mActionIdForActiveBrowsingContextInContent)) { + LOGFOCUS( + ("Ignored an attempt to set active BrowsingContext [%p] from " + "another process due to a stale action id %" PRIu64 ".", + aContext, aActionId)); + return; + } + if (aContext->IsInProcess()) { + // This message has been in transit for long enough that + // the process association of aContext has changed since + // the other content process sent the message, because + // an iframe in that process became an out-of-process + // iframe while the IPC broadcast that we're receiving + // was in-flight. Let's just ignore this. + LOGFOCUS( + ("Ignored an attempt to set an in-process BrowsingContext [%p] as " + "active from another process. actionid: %" PRIu64, + aContext, aActionId)); + return; + } + mActiveBrowsingContextInContentSetFromOtherProcess = true; + mActiveBrowsingContextInContent = aContext; + mActionIdForActiveBrowsingContextInContent = aActionId; + MaybeUnlockPointer(aContext); +} + +void nsFocusManager::UnsetActiveBrowsingContextFromOtherProcess( + BrowsingContext* aContext, uint64_t aActionId) { + MOZ_ASSERT(!XRE_IsParentProcess()); + MOZ_ASSERT(aContext); + if (ActionIdComparableAndLower(aActionId, + mActionIdForActiveBrowsingContextInContent)) { + LOGFOCUS( + ("Ignored an attempt to unset the active BrowsingContext [%p] from " + "another process due to stale action id: %" PRIu64 ".", + aContext, aActionId)); + return; + } + if (mActiveBrowsingContextInContent == aContext) { + mActiveBrowsingContextInContent = nullptr; + mActionIdForActiveBrowsingContextInContent = aActionId; + MaybeUnlockPointer(nullptr); + } else { + LOGFOCUS( + ("Ignored an attempt to unset the active BrowsingContext [%p] from " + "another process. actionid: %" PRIu64, + aContext, aActionId)); + } +} + +void nsFocusManager::ReviseActiveBrowsingContext( + uint64_t aOldActionId, mozilla::dom::BrowsingContext* aContext, + uint64_t aNewActionId) { + MOZ_ASSERT(XRE_IsContentProcess()); + if (mActionIdForActiveBrowsingContextInContent == aOldActionId) { + LOGFOCUS(("Revising the active BrowsingContext [%p]. old actionid: %" PRIu64 + ", new " + "actionid: %" PRIu64, + aContext, aOldActionId, aNewActionId)); + mActiveBrowsingContextInContent = aContext; + mActionIdForActiveBrowsingContextInContent = aNewActionId; + } else { + LOGFOCUS( + ("Ignored a stale attempt to revise the active BrowsingContext [%p]. " + "old actionid: %" PRIu64 ", new actionid: %" PRIu64, + aContext, aOldActionId, aNewActionId)); + } +} + +void nsFocusManager::ReviseFocusedBrowsingContext( + uint64_t aOldActionId, mozilla::dom::BrowsingContext* aContext, + uint64_t aNewActionId) { + MOZ_ASSERT(XRE_IsContentProcess()); + if (mActionIdForFocusedBrowsingContextInContent == aOldActionId) { + LOGFOCUS( + ("Revising the focused BrowsingContext [%p]. old actionid: %" PRIu64 + ", new " + "actionid: %" PRIu64, + aContext, aOldActionId, aNewActionId)); + mFocusedBrowsingContextInContent = aContext; + mActionIdForFocusedBrowsingContextInContent = aNewActionId; + mFocusedElement = nullptr; + } else { + LOGFOCUS( + ("Ignored a stale attempt to revise the focused BrowsingContext [%p]. " + "old actionid: %" PRIu64 ", new actionid: %" PRIu64, + aContext, aOldActionId, aNewActionId)); + } +} + +bool nsFocusManager::SetActiveBrowsingContextInChrome( + mozilla::dom::BrowsingContext* aContext, uint64_t aActionId) { + MOZ_ASSERT(aActionId); + if (ProcessPendingActiveBrowsingContextActionId(aActionId, aContext)) { + MOZ_DIAGNOSTIC_ASSERT(!ActionIdComparableAndLower( + aActionId, mActionIdForActiveBrowsingContextInChrome)); + mActiveBrowsingContextInChrome = aContext; + mActionIdForActiveBrowsingContextInChrome = aActionId; + return true; + } + return false; +} + +uint64_t nsFocusManager::GetActionIdForActiveBrowsingContextInChrome() const { + return mActionIdForActiveBrowsingContextInChrome; +} + +uint64_t nsFocusManager::GetActionIdForFocusedBrowsingContextInChrome() const { + return mActionIdForFocusedBrowsingContextInChrome; +} + +BrowsingContext* nsFocusManager::GetActiveBrowsingContextInChrome() { + return mActiveBrowsingContextInChrome; +} + +void nsFocusManager::InsertNewFocusActionId(uint64_t aActionId) { + LOGFOCUS(("InsertNewFocusActionId %" PRIu64, aActionId)); + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!mPendingActiveBrowsingContextActions.Contains(aActionId)); + mPendingActiveBrowsingContextActions.AppendElement(aActionId); + MOZ_ASSERT(!mPendingFocusedBrowsingContextActions.Contains(aActionId)); + mPendingFocusedBrowsingContextActions.AppendElement(aActionId); +} + +static void RemoveContentInitiatedActionsUntil( + nsTArray<uint64_t>& aPendingActions, + nsTArray<uint64_t>::index_type aUntil) { + nsTArray<uint64_t>::index_type i = 0; + while (i < aUntil) { + auto [actionProc, actionId] = + nsContentUtils::SplitProcessSpecificId(aPendingActions[i]); + Unused << actionId; + if (actionProc) { + aPendingActions.RemoveElementAt(i); + --aUntil; + continue; + } + ++i; + } +} + +bool nsFocusManager::ProcessPendingActiveBrowsingContextActionId( + uint64_t aActionId, bool aSettingToNonNull) { + MOZ_ASSERT(XRE_IsParentProcess()); + auto index = mPendingActiveBrowsingContextActions.IndexOf(aActionId); + if (index == nsTArray<uint64_t>::NoIndex) { + return false; + } + // When aSettingToNonNull is true, we need to remove one more + // element to remove the action id itself in addition to + // removing the older ones. + if (aSettingToNonNull) { + index++; + } + auto [actionProc, actionId] = + nsContentUtils::SplitProcessSpecificId(aActionId); + Unused << actionId; + if (actionProc) { + // Action from content: We allow parent-initiated actions + // to take precedence over content-initiated ones, so we + // remove only prior content-initiated actions. + RemoveContentInitiatedActionsUntil(mPendingActiveBrowsingContextActions, + index); + } else { + // Action from chrome + mPendingActiveBrowsingContextActions.RemoveElementsAt(0, index); + } + return true; +} + +bool nsFocusManager::ProcessPendingFocusedBrowsingContextActionId( + uint64_t aActionId) { + MOZ_ASSERT(XRE_IsParentProcess()); + auto index = mPendingFocusedBrowsingContextActions.IndexOf(aActionId); + if (index == nsTArray<uint64_t>::NoIndex) { + return false; + } + + auto [actionProc, actionId] = + nsContentUtils::SplitProcessSpecificId(aActionId); + Unused << actionId; + if (actionProc) { + // Action from content: We allow parent-initiated actions + // to take precedence over content-initiated ones, so we + // remove only prior content-initiated actions. + RemoveContentInitiatedActionsUntil(mPendingFocusedBrowsingContextActions, + index); + } else { + // Action from chrome + mPendingFocusedBrowsingContextActions.RemoveElementsAt(0, index); + } + return true; +} + +// static +uint64_t nsFocusManager::GenerateFocusActionId() { + uint64_t id = + nsContentUtils::GenerateProcessSpecificId(++sFocusActionCounter); + if (XRE_IsParentProcess()) { + nsFocusManager* fm = GetFocusManager(); + if (fm) { + fm->InsertNewFocusActionId(id); + } + } else { + mozilla::dom::ContentChild* contentChild = + mozilla::dom::ContentChild::GetSingleton(); + MOZ_ASSERT(contentChild); + contentChild->SendInsertNewFocusActionId(id); + } + LOGFOCUS(("GenerateFocusActionId %" PRIu64, id)); + return id; +} + +static bool IsInPointerLockContext(nsPIDOMWindowOuter* aWin) { + return PointerLockManager::IsInLockContext(aWin ? aWin->GetBrowsingContext() + : nullptr); +} + +void nsFocusManager::SetFocusedWindowInternal(nsPIDOMWindowOuter* aWindow, + uint64_t aActionId, + bool aSyncBrowsingContext) { + if (XRE_IsParentProcess() && !PointerUnlocker::sActiveUnlocker && + IsInPointerLockContext(mFocusedWindow) && + !IsInPointerLockContext(aWindow)) { + nsCOMPtr<nsIRunnable> runnable = new PointerUnlocker(); + NS_DispatchToCurrentThread(runnable); + } + + // Update the last focus time on any affected documents + if (aWindow && aWindow != mFocusedWindow) { + const TimeStamp now(TimeStamp::Now()); + for (Document* doc = aWindow->GetExtantDoc(); doc; + doc = doc->GetInProcessParentDocument()) { + doc->SetLastFocusTime(now); + } + } + + // This function may be called with zero action id to indicate that the + // action id should be ignored. + if (XRE_IsContentProcess() && aActionId && + ActionIdComparableAndLower(aActionId, + mActionIdForFocusedBrowsingContextInContent)) { + // Unclear if this ever happens. + LOGFOCUS( + ("Ignored an attempt to set an in-process BrowsingContext as " + "focused due to stale action id %" PRIu64 ".", + aActionId)); + return; + } + + mFocusedWindow = aWindow; + BrowsingContext* bc = aWindow ? aWindow->GetBrowsingContext() : nullptr; + if (aSyncBrowsingContext) { + MOZ_ASSERT(aActionId, + "aActionId must not be zero if aSyncBrowsingContext is true"); + SetFocusedBrowsingContext(bc, aActionId); + } else if (XRE_IsContentProcess()) { + MOZ_ASSERT(mFocusedBrowsingContextInContent == bc, + "Not syncing BrowsingContext even when different."); + } +} + +void nsFocusManager::NotifyOfReFocus(Element& aElement) { + nsPIDOMWindowOuter* window = GetCurrentWindow(&aElement); + if (!window || window != mFocusedWindow) { + return; + } + if (!aElement.IsInComposedDoc() || IsNonFocusableRoot(&aElement)) { + return; + } + nsIDocShell* docShell = window->GetDocShell(); + if (!docShell) { + return; + } + RefPtr<PresShell> presShell = docShell->GetPresShell(); + if (!presShell) { + return; + } + RefPtr<nsPresContext> presContext = presShell->GetPresContext(); + if (!presContext) { + return; + } + IMEStateManager::OnReFocus(*presContext, aElement); +} + +void nsFocusManager::MarkUncollectableForCCGeneration(uint32_t aGeneration) { + if (!sInstance) { + return; + } + + if (sInstance->mActiveWindow) { + sInstance->mActiveWindow->MarkUncollectableForCCGeneration(aGeneration); + } + if (sInstance->mFocusedWindow) { + sInstance->mFocusedWindow->MarkUncollectableForCCGeneration(aGeneration); + } + if (sInstance->mWindowBeingLowered) { + sInstance->mWindowBeingLowered->MarkUncollectableForCCGeneration( + aGeneration); + } + if (sInstance->mFocusedElement) { + sInstance->mFocusedElement->OwnerDoc()->MarkUncollectableForCCGeneration( + aGeneration); + } + if (sInstance->mFirstBlurEvent) { + sInstance->mFirstBlurEvent->OwnerDoc()->MarkUncollectableForCCGeneration( + aGeneration); + } + if (sInstance->mFirstFocusEvent) { + sInstance->mFirstFocusEvent->OwnerDoc()->MarkUncollectableForCCGeneration( + aGeneration); + } +} + +bool nsFocusManager::CanSkipFocus(nsIContent* aContent) { + if (!aContent) { + return false; + } + + if (mFocusedElement == aContent) { + return true; + } + + nsIDocShell* ds = aContent->OwnerDoc()->GetDocShell(); + if (!ds) { + return true; + } + + if (XRE_IsParentProcess()) { + nsCOMPtr<nsIDocShellTreeItem> root; + ds->GetInProcessRootTreeItem(getter_AddRefs(root)); + nsCOMPtr<nsPIDOMWindowOuter> newRootWindow = + root ? root->GetWindow() : nullptr; + if (mActiveWindow != newRootWindow) { + nsPIDOMWindowOuter* outerWindow = aContent->OwnerDoc()->GetWindow(); + if (outerWindow && outerWindow->GetFocusedElement() == aContent) { + return true; + } + } + } else { + BrowsingContext* bc = aContent->OwnerDoc()->GetBrowsingContext(); + BrowsingContext* top = bc ? bc->Top() : nullptr; + if (GetActiveBrowsingContext() != top) { + nsPIDOMWindowOuter* outerWindow = aContent->OwnerDoc()->GetWindow(); + if (outerWindow && outerWindow->GetFocusedElement() == aContent) { + return true; + } + } + } + + return false; +} + +/* static */ +Element* nsFocusManager::GetTheFocusableArea(Element* aTarget, + uint32_t aFlags) { + MOZ_ASSERT(aTarget); + nsIFrame* frame = aTarget->GetPrimaryFrame(); + if (!frame) { + return nullptr; + } + + // If focus target is the document element of its Document. + if (aTarget == aTarget->OwnerDoc()->GetRootElement()) { + // the root content can always be focused, + // except in userfocusignored context. + return aTarget; + } + + // If focus target is an area element with one or more shapes that are + // focusable areas. + if (auto* area = HTMLAreaElement::FromNode(aTarget)) { + return IsAreaElementFocusable(*area) ? area : nullptr; + } + + // For these 3 steps mentioned in the spec + // 1. If focus target is an element with one or more scrollable regions that + // are focusable areas + // 2. If focus target is a navigable + // 3. If focus target is a navigable container with a non-null content + // navigable + // nsIFrame::IsFocusable will effectively perform the checks for them. + if (frame->IsFocusable(aFlags & FLAG_BYMOUSE)) { + return aTarget; + } + + // If focus target is a shadow host whose shadow root's delegates focus is + // true + if (ShadowRoot* root = aTarget->GetShadowRoot()) { + if (root->DelegatesFocus()) { + // If focus target is a shadow-including inclusive ancestor of the + // currently focused area of a top-level browsing context's DOM anchor, + // then return the already-focused element. + if (nsPIDOMWindowInner* innerWindow = + aTarget->OwnerDoc()->GetInnerWindow()) { + if (Element* focusedElement = innerWindow->GetFocusedElement()) { + if (focusedElement->IsShadowIncludingInclusiveDescendantOf(aTarget)) { + return focusedElement; + } + } + } + + if (Element* firstFocusable = + root->GetFocusDelegate(aFlags & FLAG_BYMOUSE)) { + return firstFocusable; + } + } + } + return nullptr; +} + +/* static */ +bool nsFocusManager::IsAreaElementFocusable(HTMLAreaElement& aArea) { + nsIFrame* frame = aArea.GetPrimaryFrame(); + if (!frame) { + return false; + } + // HTML areas do not have their own frame, and the img frame we get from + // GetPrimaryFrame() is not relevant as to whether it is focusable or + // not, so we have to do all the relevant checks manually for them. + return frame->IsVisibleConsideringAncestors() && + aArea.IsFocusableWithoutStyle(false /* aWithMouse */); +} + +nsresult NS_NewFocusManager(nsIFocusManager** aResult) { + NS_IF_ADDREF(*aResult = nsFocusManager::GetFocusManager()); + return NS_OK; +} |