gecko-dev/dom/base/nsFocusManager.cpp
Emilio Cobos Álvarez 8f4f441cae Bug 1388910 - Remove -moz-user-modify property. r=dshin
We successfully unshipped it in bug 1920118.

Differential Revision: https://phabricator.services.mozilla.com/D229530
2024-11-19 19:35:35 +00:00

5681 lines
211 KiB
C++

/* -*- 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 "nsThreadUtils.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 "nsIXULRuntime.h"
#include "mozilla/AccessibleCaretEventHub.h"
#include "mozilla/ContentEvents.h"
#include "mozilla/FocusModel.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",
"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();
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("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;
}
NS_IMETHODIMP
nsFocusManager::GetActiveContentBrowsingContext(
BrowsingContext** aBrowsingContext) {
MOZ_DIAGNOSTIC_ASSERT(
XRE_IsParentProcess(),
"We only have use cases for this in the parent process");
NS_IF_ADDREF(*aBrowsingContext = GetActiveBrowsingContextInChrome());
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> existingActionId;
if (frameElement) {
// pass false for aFocusChanged so that the caret does not get updated
// and scrolling does not occur.
existingActionId = SetFocusInner(frameElement, 0, false, true);
} else if (auto* bc = windowToFocus->GetBrowsingContext();
bc && !bc->IsTop()) {
// No frameElement means windowToFocus is an OOP iframe, so
// the above SetFocusInner is not called. That means the focus
// of the currently focused BC is not going to be cleared. So
// we do that manually here.
if (RefPtr<BrowsingContext> focusedBC = GetFocusedBrowsingContext()) {
// If focusedBC is an ancestor of bc, blur will be handled
// correctly by nsFocusManager::AdjustWindowFocus.
if (!IsSameOrAncestor(focusedBC, bc)) {
existingActionId.emplace(sInstance->GenerateFocusActionId());
Blur(focusedBC, nullptr, true, true, false, existingActionId.value());
}
}
} 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 = existingActionId.isSome()
? existingActionId.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();
RefPtr<BrowsingContext> focusedBC = GetFocusedBrowsingContext();
const bool isAncestor = (focusedBC != bc);
RefPtr<BrowsingContext> ancestorBC = isAncestor ? bc : nullptr;
if (Blur(focusedBC, ancestorBC, isAncestor, true, false,
GenerateFocusActionId())) {
// 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) {
// Intentionally use a new actionId here because the above
// Blur() will clear the focus of the ancestors of focusedBC, and
// this Focus() call might need to update the focus of those ancestors,
// so it needs to have a newer actionId to make that happen.
Focus(window, nullptr, 0, true, false, false, true,
GenerateFocusActionId());
}
}
} 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,
false /* aIsEnteringBFCache */);
}
// 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,
false /* aIsEnteringBFCache */);
}
}
if (mFocusedWindow) {
Blur(nullptr, nullptr, true, true, false, aActionId);
}
mWindowBeingLowered = nullptr;
}
nsresult nsFocusManager::ContentRemoved(Document* aDocument,
nsIContent* aContent) {
NS_ENSURE_ARG(aDocument);
NS_ENSURE_ARG(aContent);
nsPIDOMWindowOuter* windowPtr = aDocument->GetWindow();
if (!windowPtr) {
return NS_OK;
}
// if the content is currently focused in the window, or is an
// shadow-including inclusive ancestor of the currently focused element,
// reset the focus within that window.
Element* previousFocusedElementPtr = windowPtr->GetFocusedElement();
if (!previousFocusedElementPtr) {
return NS_OK;
}
if (!nsContentUtils::ContentIsHostIncludingDescendantOf(
previousFocusedElementPtr, aContent)) {
return NS_OK;
}
RefPtr<nsPIDOMWindowOuter> window = windowPtr;
RefPtr<Element> previousFocusedElement = previousFocusedElementPtr;
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 (nsIDocShell* const docShell = aDocument->GetDocShell()) {
if (HTMLEditor* const htmlEditor = docShell->GetHTMLEditor()) {
Selection* const selection = htmlEditor->GetSelection();
if (selection && selection->GetFrameSelection() &&
previousFocusedElement ==
selection->GetFrameSelection()->GetAncestorLimiter()) {
// The editing host may be being removed right now. So, it's already
// removed from the child chain of the parent node, but it still know
// the parent node. This could cause unexpected result at scheduling
// paint of the caret. Therefore, we should call FinalizeSelection
// after unblocking to run the script.
nsContentUtils::AddScriptRunner(
NewRunnableMethod("HTMLEditor::FinalizeSelection", htmlEditor,
&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, bool aIsEnteringBFCache) {
// 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, aIsEnteringBFCache);
}
// 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(),
SourceLocation(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, false /* aShouldClearAncestorFocus */,
nullptr /* aAncestorBrowsingContextToFocus */);
}
// 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, bool aShouldClearAncestorFocus,
BrowsingContext* aAncestorBrowsingContextToFocus) {
MOZ_ASSERT_IF(aAncestorBrowsingContextToFocus, aShouldClearAncestorFocus);
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 (aShouldClearAncestorFocus) {
// This is the BrowsingContext that receives the focus, no need to clear
// its focused element and the rest of the ancestors.
if (window->GetBrowsingContext() == aAncestorBrowsingContextToFocus) {
break;
}
window->SetFocusedElement(nullptr);
continue;
}
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, bool aShouldClearAncestorFocus,
BrowsingContext* aAncestorBrowsingContextToFocus) {
MOZ_ASSERT_IF(aAncestorBrowsingContextToFocus, aShouldClearAncestorFocus);
if (AdjustInProcessWindowFocus(aBrowsingContext, aCheckPermission, aIsVisible,
aActionId, aShouldClearAncestorFocus,
aAncestorBrowsingContextToFocus)) {
// 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,
aShouldClearAncestorFocus,
aAncestorBrowsingContextToFocus);
}
}
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 documentViewer-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);
}
// When the focus of aBrowsingContextToClear is cleared, it should
// also clear its ancestors's focus because ancestors should no longer
// be considered aBrowsingContextToClear is focused.
//
// We don't need to do this when aBrowsingContextToClear and
// aAncestorBrowsingContextToFocus is equal because ancestors don't
// care about this.
if (aBrowsingContextToClear &&
aBrowsingContextToClear != aAncestorBrowsingContextToFocus) {
AdjustWindowFocus(
aBrowsingContextToClear, false,
IsWindowVisible(aBrowsingContextToClear->GetDOMWindow()), aActionId,
true /* aShouldClearAncestorFocus */,
aAncestorBrowsingContextToFocus);
}
}
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,
false /* aShouldClearAncestorFocus */,
nullptr /* aAncestorBrowsingContextToFocus */);
}
// 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);
// 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;
// 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, false, aNextContent);
}
if (aType == MOVEFOCUS_LAST) {
if (!aStartContent) {
startContent = rootElement;
}
return GetNextTabbableContent(presShell, startContent, nullptr,
startContent, false, 0, false, false,
aNavigateByKey, false, 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 selection starts from a focusable and tabbable element, we want
// to make it focused rather than next/previous one.
if (startContent->IsElement() && startContent->GetPrimaryFrame() &&
startContent->GetPrimaryFrame()->IsFocusable().IsTabbable()) {
startContent =
forward ? (startContent->GetPreviousSibling()
? startContent->GetPreviousSibling()
// We don't need to get previous leaf node
// because it may be too far from
// startContent. We just want the previous
// node immediately before startContent.
: startContent->GetParent())
// We want the next node immdiately after startContent.
// Therefore, we don't want its first child.
: startContent->GetNextNonChildNode();
// If we reached the root element, we should treat it as there is no
// selection as same as above.
if (startContent == rootElement) {
startContent = nullptr;
}
}
}
}
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, 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 move the focus to the parent process.
if (auto* child = BrowserChild::GetFrom(docShell)) {
child->SendMoveFocus(forward, forDocumentNavigation);
// Blur the current element.
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 by
// pressing F6(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,
true /* aReachedToEndForDocumentNavigation */);
}
// 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, bool aReachedToEndForDocumentNavigation) {
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) {
if (aReachedToEndForDocumentNavigation &&
StaticPrefs::dom_disable_tab_focus_to_root_element() &&
nsContentUtils::IsChromeDoc(iterContent->GetComposedDoc())) {
// aReachedToEndForDocumentNavigation is true means
// 1. This is a document navigation (i.e, VK_F6, Control + Tab)
// 2. This is the top-level document (Note that we may start from
// a subdocument)
// 3. We've searched through the this top-level document already
if (!GetRootForChildDocument(iterContent)) {
// We'd like to focus the first focusable element of this
// top-level chrome document.
return iterContent;
}
}
continue;
}
if (TryToMoveFocusToSubDocument(iterContent, aOriginalStartContent,
aForward, aForDocumentNavigation,
aNavigateByKey,
aReachedToEndForDocumentNavigation,
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 */,
aReachedToEndForDocumentNavigation);
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,
bool aReachedToEndForDocumentNavigation) {
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 */, aReachedToEndForDocumentNavigation);
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,
bool aReachedToEndForDocumentNavigation, 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 */, aReachedToEndForDocumentNavigation);
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 */, aReachedToEndForDocumentNavigation);
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, aReachedToEndForDocumentNavigation);
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 */,
aReachedToEndForDocumentNavigation);
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 the invoker is
// focusable. Otherwise search for the next tabbable content after
// invoker.
if (oldTopLevelScopeOwner &&
IsOpenPopoverWithInvoker(oldTopLevelScopeOwner) &&
currentTopLevelScopeOwner != oldTopLevelScopeOwner) {
if (auto* popover = Element::FromNode(oldTopLevelScopeOwner)) {
RefPtr<nsIContent> invokerContent =
popover->GetPopoverData()->GetInvoker()->AsContent();
RefPtr<nsIContent> rootElement = invokerContent;
if (auto* doc = invokerContent->GetComposedDoc()) {
rootElement = doc->GetRootElement();
}
if (aForward) {
nsIFrame* frame = invokerContent->GetPrimaryFrame();
int32_t tabIndex = frame->IsFocusable().mTabIndex;
if (tabIndex >= 0 &&
(aIgnoreTabIndex || aCurrentTabIndex == tabIndex)) {
nsresult rv = GetNextTabbableContent(
aPresShell, rootElement, nullptr, invokerContent, true,
tabIndex, false, false, aNavigateByKey, true,
aReachedToEndForDocumentNavigation, aResultContent);
if (NS_SUCCEEDED(rv) && *aResultContent) {
return rv;
}
}
} else if (invokerContent) {
nsIFrame* frame = invokerContent->GetPrimaryFrame();
if (frame && frame->IsFocusable()) {
invokerContent.forget(aResultContent);
return NS_OK;
}
nsresult rv = GetNextTabbableContent(
aPresShell, rootElement, aOriginalStartContent, invokerContent,
false, 0, true, false, aNavigateByKey, true,
aReachedToEndForDocumentNavigation, aResultContent);
if (NS_SUCCEEDED(rv) && *aResultContent) {
return rv;
}
}
}
}
if (!aForward) {
if (InvokerForPopoverShowingState(currentContent)) {
RefPtr<nsIContent> popover =
currentContent->GetEffectivePopoverTargetElement();
nsIContent* contentToFocus = GetNextTabbableContentInScope(
popover, popover, aOriginalStartContent, aForward, 0,
aIgnoreTabIndex, aForDocumentNavigation, aNavigateByKey,
true /* aSkipOwner */, aReachedToEndForDocumentNavigation);
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,
aReachedToEndForDocumentNavigation, 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 */, aReachedToEndForDocumentNavigation);
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,
aReachedToEndForDocumentNavigation, 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 (currentContent && aReachedToEndForDocumentNavigation &&
StaticPrefs::dom_disable_tab_focus_to_root_element() &&
nsContentUtils::IsChromeDoc(
currentContent->GetComposedDoc())) {
// aReachedToEndForDocumentNavigation is true means
// 1. This is a document navigation (i.e, VK_F6, Control + Tab)
// 2. This is the top-level document (Note that we may start from
// a subdocument)
// 3. We've searched through the this top-level document already
if (!GetRootForChildDocument(currentContent)) {
// We'd like to focus the first focusable element of this
// top-level chrome document.
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 && !StaticPrefs::dom_disable_tab_focus_to_root_element()) {
nsCOMPtr<nsPIDOMWindowOuter> window = GetCurrentWindow(aRootContent);
NS_ENSURE_TRUE(window, NS_ERROR_FAILURE);
RefPtr<Element> docRoot = GetRootForFocus(
window, aRootContent->GetComposedDoc(), false, true);
FocusFirst(docRoot, aResultContent,
false /* aReachedToEndForDocumentNavigation */);
}
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,
false /* aReachedToEndForDocumentNavigation */);
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,
bool aReachedToEndForDocumentNavigation, nsIContent** aResultContent) {
Document* doc = aCurrentContent->GetComposedDoc();
NS_ASSERTION(doc, "content not in document");
Document* subdoc = doc->GetSubDocumentFor(aCurrentContent);
if (subdoc && !subdoc->EventHandlingSuppressed()) {
if (aForward && !StaticPrefs::dom_disable_tab_focus_to_root_element()) {
// 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, aReachedToEndForDocumentNavigation,
aResultContent);
NS_ENSURE_SUCCESS(rv, false);
if (*aResultContent) {
return true;
}
if (rootElement->IsEditable() &&
StaticPrefs::dom_disable_tab_focus_to_root_element()) {
// Only move to the root element with a valid reason
*aResultContent = rootElement;
NS_ADDREF(*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,
bool aReachedToEndForDocumentNavigation) {
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,
StaticPrefs::dom_disable_tab_focus_to_root_element()
? aReachedToEndForDocumentNavigation
: false,
true, false, aReachedToEndForDocumentNavigation, 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("FocusChange");
}
}
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,
bool aIsEnteringBFCache) {
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 &&
!(BFCacheInParent() && aIsEnteringBFCache)) {
// No need to tell the parent process to update the active browsing
// context to null if we are entering BFCache, because the browsing
// context that is about to show will update it.
//
// 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 IsFocusableFlags FocusManagerFlagsToIsFocusableFlags(uint32_t aFlags) {
auto flags = IsFocusableFlags(0);
if (aFlags & nsIFocusManager::FLAG_BYMOUSE) {
flags |= IsFocusableFlags::WithMouse;
}
return flags;
}
/* 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.
IsFocusableFlags flags = FocusManagerFlagsToIsFocusableFlags(aFlags);
if (frame->IsFocusable(flags)) {
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(flags)) {
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();
}
nsresult NS_NewFocusManager(nsIFocusManager** aResult) {
NS_IF_ADDREF(*aResult = nsFocusManager::GetFocusManager());
return NS_OK;
}