Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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 "AccessibleCaretManager.h"
#include <utility>
#include "AccessibleCaret.h"
#include "AccessibleCaretEventHub.h"
#include "AccessibleCaretLogger.h"
#include "mozilla/AsyncEventDispatcher.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/CaretAssociationHint.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/MouseEventBinding.h"
#include "mozilla/dom/NodeFilterBinding.h"
#include "mozilla/dom/Selection.h"
#include "mozilla/dom/TreeWalker.h"
#include "mozilla/IMEStateManager.h"
#include "mozilla/IntegerPrintfMacros.h"
#include "mozilla/PresShell.h"
#include "mozilla/SelectionMovementUtils.h"
#include "mozilla/StaticAnalysisFunctions.h"
#include "mozilla/StaticPrefs_layout.h"
#include "nsCaret.h"
#include "nsContainerFrame.h"
#include "nsContentUtils.h"
#include "nsDebug.h"
#include "nsFocusManager.h"
#include "nsIFrame.h"
#include "nsFrameSelection.h"
#include "nsGenericHTMLElement.h"
#include "nsIHapticFeedback.h"
#include "nsIScrollableFrame.h"
#include "nsLayoutUtils.h"
#include "nsServiceManagerUtils.h"
namespace mozilla {
#undef AC_LOG
#define AC_LOG(message, ...) \
AC_LOG_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
#undef AC_LOGV
#define AC_LOGV(message, ...) \
AC_LOGV_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
using namespace dom;
using Appearance = AccessibleCaret::Appearance;
using PositionChangedResult = AccessibleCaret::PositionChangedResult;
#define AC_PROCESS_ENUM_TO_STREAM(e) \
case (e): \
aStream << #e; \
break;
std::ostream& operator<<(std::ostream& aStream,
const AccessibleCaretManager::CaretMode& aCaretMode) {
using CaretMode = AccessibleCaretManager::CaretMode;
switch (aCaretMode) {
AC_PROCESS_ENUM_TO_STREAM(CaretMode::None);
AC_PROCESS_ENUM_TO_STREAM(CaretMode::Cursor);
AC_PROCESS_ENUM_TO_STREAM(CaretMode::Selection);
}
return aStream;
}
std::ostream& operator<<(
std::ostream& aStream,
const AccessibleCaretManager::UpdateCaretsHint& aHint) {
using UpdateCaretsHint = AccessibleCaretManager::UpdateCaretsHint;
switch (aHint) {
AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::Default);
AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::RespectOldAppearance);
AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::DispatchNoEvent);
}
return aStream;
}
#undef AC_PROCESS_ENUM_TO_STREAM
AccessibleCaretManager::AccessibleCaretManager(PresShell* aPresShell)
: AccessibleCaretManager{
aPresShell,
Carets{aPresShell ? MakeUnique<AccessibleCaret>(aPresShell) : nullptr,
aPresShell ? MakeUnique<AccessibleCaret>(aPresShell)
: nullptr}} {}
AccessibleCaretManager::AccessibleCaretManager(PresShell* aPresShell,
Carets aCarets)
: mPresShell{aPresShell}, mCarets{std::move(aCarets)} {}
AccessibleCaretManager::LayoutFlusher::~LayoutFlusher() {
MOZ_RELEASE_ASSERT(!mFlushing, "Going away in MaybeFlush? Bad!");
}
void AccessibleCaretManager::Terminate() {
mCarets.Terminate();
mActiveCaret = nullptr;
mPresShell = nullptr;
}
nsresult AccessibleCaretManager::OnSelectionChanged(Document* aDoc,
Selection* aSel,
int16_t aReason) {
Selection* selection = GetSelection();
AC_LOG("%s: aSel: %p, GetSelection(): %p, aReason: %d", __FUNCTION__, aSel,
selection, aReason);
if (aSel != selection) {
return NS_OK;
}
// eSetSelection events from the Fennec widget IME can be generated
// by autoSuggest / autoCorrect composition changes, or by TYPE_REPLACE_TEXT
// actions, either positioning cursor for text insert, or selecting
// text-to-be-replaced. None should affect AccessibleCaret visibility.
if (aReason & nsISelectionListener::IME_REASON) {
return NS_OK;
}
// Move the cursor by JavaScript or unknown internal call.
if (aReason == nsISelectionListener::NO_REASON ||
aReason == nsISelectionListener::JS_REASON) {
auto mode = static_cast<ScriptUpdateMode>(
StaticPrefs::layout_accessiblecaret_script_change_update_mode());
if (mode == kScriptAlwaysShow ||
(mode == kScriptUpdateVisible && mCarets.HasLogicallyVisibleCaret())) {
UpdateCarets();
return NS_OK;
}
// Default for NO_REASON is to make hidden.
HideCaretsAndDispatchCaretStateChangedEvent();
return NS_OK;
}
// Move cursor by keyboard.
if (aReason & nsISelectionListener::KEYPRESS_REASON) {
HideCaretsAndDispatchCaretStateChangedEvent();
return NS_OK;
}
// OnBlur() might be called between mouse down and mouse up, so we hide carets
// upon mouse down anyway, and update carets upon mouse up.
if (aReason & nsISelectionListener::MOUSEDOWN_REASON) {
HideCaretsAndDispatchCaretStateChangedEvent();
return NS_OK;
}
// Range will collapse after cutting or copying text.
if (aReason & (nsISelectionListener::COLLAPSETOSTART_REASON |
nsISelectionListener::COLLAPSETOEND_REASON)) {
HideCaretsAndDispatchCaretStateChangedEvent();
return NS_OK;
}
// For mouse input we don't want to show the carets.
if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_MOUSE) {
HideCaretsAndDispatchCaretStateChangedEvent();
return NS_OK;
}
// When we want to hide the carets for mouse input, hide them for select
// all action fired by keyboard as well.
if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_KEYBOARD &&
(aReason & nsISelectionListener::SELECTALL_REASON)) {
HideCaretsAndDispatchCaretStateChangedEvent();
return NS_OK;
}
UpdateCarets();
return NS_OK;
}
void AccessibleCaretManager::HideCaretsAndDispatchCaretStateChangedEvent() {
if (mCarets.HasLogicallyVisibleCaret()) {
AC_LOG("%s", __FUNCTION__);
mCarets.GetFirst()->SetAppearance(Appearance::None);
mCarets.GetSecond()->SetAppearance(Appearance::None);
mIsCaretPositionChanged = false;
DispatchCaretStateChangedEvent(CaretChangedReason::Visibilitychange);
}
}
auto AccessibleCaretManager::MaybeFlushLayout() -> Terminated {
if (mPresShell) {
// `MaybeFlush` doesn't access the PresShell after flushing, so it's OK to
// mark it as live.
mLayoutFlusher.MaybeFlush(MOZ_KnownLive(*mPresShell));
}
return IsTerminated();
}
void AccessibleCaretManager::UpdateCarets(const UpdateCaretsHintSet& aHint) {
if (MaybeFlushLayout() == Terminated::Yes) {
return;
}
mLastUpdateCaretMode = GetCaretMode();
switch (mLastUpdateCaretMode) {
case CaretMode::None:
HideCaretsAndDispatchCaretStateChangedEvent();
break;
case CaretMode::Cursor:
UpdateCaretsForCursorMode(aHint);
break;
case CaretMode::Selection:
UpdateCaretsForSelectionMode(aHint);
break;
}
mDesiredAsyncPanZoomState.Update(*this);
}
bool AccessibleCaretManager::IsCaretDisplayableInCursorMode(
nsIFrame** aOutFrame, int32_t* aOutOffset) const {
RefPtr<nsCaret> caret = mPresShell->GetCaret();
if (!caret || !caret->IsVisible()) {
return false;
}
auto frameData =
nsCaret::GetFrameAndOffset(nsCaret::CaretPositionFor(GetSelection()));
if (!GetEditingHostForFrame(frameData.mFrame)) {
return false;
}
if (aOutFrame) {
*aOutFrame = frameData.mFrame;
}
if (aOutOffset) {
*aOutOffset = frameData.mOffsetInFrameContent;
}
return true;
}
bool AccessibleCaretManager::HasNonEmptyTextContent(nsINode* aNode) const {
return nsContentUtils::HasNonEmptyTextContent(
aNode, nsContentUtils::eRecurseIntoChildren);
}
void AccessibleCaretManager::UpdateCaretsForCursorMode(
const UpdateCaretsHintSet& aHints) {
AC_LOG("%s, selection: %p", __FUNCTION__, GetSelection());
int32_t offset = 0;
nsIFrame* frame = nullptr;
if (!IsCaretDisplayableInCursorMode(&frame, &offset)) {
HideCaretsAndDispatchCaretStateChangedEvent();
return;
}
PositionChangedResult result = mCarets.GetFirst()->SetPosition(frame, offset);
switch (result) {
case PositionChangedResult::NotChanged:
case PositionChangedResult::Position:
case PositionChangedResult::Zoom:
if (!aHints.contains(UpdateCaretsHint::RespectOldAppearance)) {
if (HasNonEmptyTextContent(GetEditingHostForFrame(frame))) {
mCarets.GetFirst()->SetAppearance(Appearance::Normal);
} else if (
StaticPrefs::
layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
if (mCarets.GetFirst()->IsLogicallyVisible()) {
// Possible cases are: 1) SelectWordOrShortcut() sets the
// appearance to Normal. 2) When the caret is out of viewport and
// now scrolling into viewport, it has appearance NormalNotShown.
mCarets.GetFirst()->SetAppearance(Appearance::Normal);
} else {
// Possible cases are: a) Single tap on current empty content;
// OnSelectionChanged() sets the appearance to None due to
// MOUSEDOWN_REASON. b) Single tap on other empty content;
// OnBlur() sets the appearance to None.
//
// Do nothing to make the appearance remains None so that it can
// be distinguished from case 2). Also do not set the appearance
// to NormalNotShown here like the default update behavior.
}
} else {
mCarets.GetFirst()->SetAppearance(Appearance::NormalNotShown);
}
}
break;
case PositionChangedResult::Invisible:
mCarets.GetFirst()->SetAppearance(Appearance::NormalNotShown);
break;
}
mCarets.GetSecond()->SetAppearance(Appearance::None);
mIsCaretPositionChanged = (result == PositionChangedResult::Position);
if (!aHints.contains(UpdateCaretsHint::DispatchNoEvent) && !mActiveCaret) {
DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition);
}
}
void AccessibleCaretManager::UpdateCaretsForSelectionMode(
const UpdateCaretsHintSet& aHints) {
AC_LOG("%s: selection: %p", __FUNCTION__, GetSelection());
int32_t startOffset = 0;
nsIFrame* startFrame =
GetFrameForFirstRangeStartOrLastRangeEnd(eDirNext, &startOffset);
int32_t endOffset = 0;
nsIFrame* endFrame =
GetFrameForFirstRangeStartOrLastRangeEnd(eDirPrevious, &endOffset);
if (!CompareTreePosition(startFrame, endFrame)) {
// XXX: Do we really have to hide carets if this condition isn't satisfied?
HideCaretsAndDispatchCaretStateChangedEvent();
return;
}
auto updateSingleCaret = [aHints](AccessibleCaret* aCaret, nsIFrame* aFrame,
int32_t aOffset) -> PositionChangedResult {
PositionChangedResult result = aCaret->SetPosition(aFrame, aOffset);
switch (result) {
case PositionChangedResult::NotChanged:
case PositionChangedResult::Position:
case PositionChangedResult::Zoom:
if (!aHints.contains(UpdateCaretsHint::RespectOldAppearance)) {
aCaret->SetAppearance(Appearance::Normal);
}
break;
case PositionChangedResult::Invisible:
aCaret->SetAppearance(Appearance::NormalNotShown);
break;
}
return result;
};
PositionChangedResult firstCaretResult =
updateSingleCaret(mCarets.GetFirst(), startFrame, startOffset);
PositionChangedResult secondCaretResult =
updateSingleCaret(mCarets.GetSecond(), endFrame, endOffset);
mIsCaretPositionChanged =
firstCaretResult == PositionChangedResult::Position ||
secondCaretResult == PositionChangedResult::Position;
if (mIsCaretPositionChanged) {
// Flush layout to make the carets intersection correct.
if (MaybeFlushLayout() == Terminated::Yes) {
return;
}
}
if (!aHints.contains(UpdateCaretsHint::RespectOldAppearance)) {
// Only check for tilt carets when the caller doesn't ask us to preserve
// old appearance. Otherwise we might override the appearance set by the
// caller.
if (StaticPrefs::layout_accessiblecaret_always_tilt()) {
UpdateCaretsForAlwaysTilt(startFrame, endFrame);
} else {
UpdateCaretsForOverlappingTilt();
}
}
if (!aHints.contains(UpdateCaretsHint::DispatchNoEvent) && !mActiveCaret) {
DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition);
}
}
void AccessibleCaretManager::DesiredAsyncPanZoomState::Update(
const AccessibleCaretManager& aAccessibleCaretManager) {
if (aAccessibleCaretManager.mActiveCaret) {
// No need to disable APZ when dragging the caret.
mValue = Value::Enabled;
return;
}
if (aAccessibleCaretManager.mIsScrollStarted) {
// During scrolling, the caret's position is changed only if it is in a
// position:fixed or a "stuck" position:sticky frame subtree.
mValue = aAccessibleCaretManager.mIsCaretPositionChanged ? Value::Disabled
: Value::Enabled;
return;
}
// For other cases, we can only reliably detect whether the caret is in a
// position:fixed frame subtree.
switch (aAccessibleCaretManager.mLastUpdateCaretMode) {
case CaretMode::None:
mValue = Value::Enabled;
break;
case CaretMode::Cursor:
mValue =
(aAccessibleCaretManager.mCarets.GetFirst()->IsVisuallyVisible() &&
aAccessibleCaretManager.mCarets.GetFirst()
->IsInPositionFixedSubtree())
? Value::Disabled
: Value::Enabled;
break;
case CaretMode::Selection:
mValue =
((aAccessibleCaretManager.mCarets.GetFirst()->IsVisuallyVisible() &&
aAccessibleCaretManager.mCarets.GetFirst()
->IsInPositionFixedSubtree()) ||
(aAccessibleCaretManager.mCarets.GetSecond()->IsVisuallyVisible() &&
aAccessibleCaretManager.mCarets.GetSecond()
->IsInPositionFixedSubtree()))
? Value::Disabled
: Value::Enabled;
break;
}
}
bool AccessibleCaretManager::UpdateCaretsForOverlappingTilt() {
if (!mCarets.GetFirst()->IsVisuallyVisible() ||
!mCarets.GetSecond()->IsVisuallyVisible()) {
return false;
}
if (!mCarets.GetFirst()->Intersects(*mCarets.GetSecond())) {
mCarets.GetFirst()->SetAppearance(Appearance::Normal);
mCarets.GetSecond()->SetAppearance(Appearance::Normal);
return false;
}
if (mCarets.GetFirst()->LogicalPosition().x <=
mCarets.GetSecond()->LogicalPosition().x) {
mCarets.GetFirst()->SetAppearance(Appearance::Left);
mCarets.GetSecond()->SetAppearance(Appearance::Right);
} else {
mCarets.GetFirst()->SetAppearance(Appearance::Right);
mCarets.GetSecond()->SetAppearance(Appearance::Left);
}
return true;
}
void AccessibleCaretManager::UpdateCaretsForAlwaysTilt(
const nsIFrame* aStartFrame, const nsIFrame* aEndFrame) {
// When a short LTR word in RTL environment is selected, the two carets
// tilted inward might be overlapped. Make them tilt outward.
if (UpdateCaretsForOverlappingTilt()) {
return;
}
if (mCarets.GetFirst()->IsVisuallyVisible()) {
auto startFrameWritingMode = aStartFrame->GetWritingMode();
mCarets.GetFirst()->SetAppearance(startFrameWritingMode.IsBidiLTR()
? Appearance::Left
: Appearance::Right);
}
if (mCarets.GetSecond()->IsVisuallyVisible()) {
auto endFrameWritingMode = aEndFrame->GetWritingMode();
mCarets.GetSecond()->SetAppearance(
endFrameWritingMode.IsBidiLTR() ? Appearance::Right : Appearance::Left);
}
}
void AccessibleCaretManager::ProvideHapticFeedback() {
if (StaticPrefs::layout_accessiblecaret_hapticfeedback()) {
if (nsCOMPtr<nsIHapticFeedback> haptic =
do_GetService("@mozilla.org/widget/hapticfeedback;1")) {
haptic->PerformSimpleAction(haptic->LongPress);
}
}
}
nsresult AccessibleCaretManager::PressCaret(const nsPoint& aPoint,
EventClassID aEventClass) {
nsresult rv = NS_ERROR_FAILURE;
MOZ_ASSERT(aEventClass == eMouseEventClass || aEventClass == eTouchEventClass,
"Unexpected event class!");
using TouchArea = AccessibleCaret::TouchArea;
TouchArea touchArea =
aEventClass == eMouseEventClass ? TouchArea::CaretImage : TouchArea::Full;
if (mCarets.GetFirst()->Contains(aPoint, touchArea)) {
mActiveCaret = mCarets.GetFirst();
SetSelectionDirection(eDirPrevious);
} else if (mCarets.GetSecond()->Contains(aPoint, touchArea)) {
mActiveCaret = mCarets.GetSecond();
SetSelectionDirection(eDirNext);
}
if (mActiveCaret) {
mOffsetYToCaretLogicalPosition =
mActiveCaret->LogicalPosition().y - aPoint.y;
SetSelectionDragState(true);
DispatchCaretStateChangedEvent(CaretChangedReason::Presscaret, &aPoint);
rv = NS_OK;
}
return rv;
}
nsresult AccessibleCaretManager::DragCaret(const nsPoint& aPoint) {
MOZ_ASSERT(mActiveCaret);
MOZ_ASSERT(GetCaretMode() != CaretMode::None);
if (!mPresShell || !mPresShell->GetRootFrame() || !GetSelection()) {
return NS_ERROR_NULL_POINTER;
}
StopSelectionAutoScrollTimer();
DragCaretInternal(aPoint);
// We want to scroll the page even if we failed to drag the caret.
StartSelectionAutoScrollTimer(aPoint);
UpdateCarets();
if (StaticPrefs::layout_accessiblecaret_magnifier_enabled()) {
DispatchCaretStateChangedEvent(CaretChangedReason::Dragcaret, &aPoint);
}
return NS_OK;
}
nsresult AccessibleCaretManager::ReleaseCaret() {
MOZ_ASSERT(mActiveCaret);
mActiveCaret = nullptr;
SetSelectionDragState(false);
mDesiredAsyncPanZoomState.Update(*this);
DispatchCaretStateChangedEvent(CaretChangedReason::Releasecaret);
return NS_OK;
}
nsresult AccessibleCaretManager::TapCaret(const nsPoint& aPoint) {
MOZ_ASSERT(GetCaretMode() != CaretMode::None);
nsresult rv = NS_ERROR_FAILURE;
if (GetCaretMode() == CaretMode::Cursor) {
DispatchCaretStateChangedEvent(CaretChangedReason::Taponcaret, &aPoint);
rv = NS_OK;
}
return rv;
}
static EnumSet<nsLayoutUtils::FrameForPointOption> GetHitTestOptions() {
EnumSet<nsLayoutUtils::FrameForPointOption> options = {
nsLayoutUtils::FrameForPointOption::IgnorePaintSuppression,
nsLayoutUtils::FrameForPointOption::IgnoreCrossDoc};
return options;
}
nsresult AccessibleCaretManager::SelectWordOrShortcut(const nsPoint& aPoint) {
// If the long-tap is landing on a pre-existing selection, don't replace
// it with a new one. Instead just return and let the context menu pop up
// on the pre-existing selection.
if (GetCaretMode() == CaretMode::Selection &&
GetSelection()->ContainsPoint(aPoint)) {
AC_LOG("%s: UpdateCarets() for current selection", __FUNCTION__);
UpdateCarets();
ProvideHapticFeedback();
return NS_OK;
}
if (!mPresShell) {
return NS_ERROR_UNEXPECTED;
}
nsIFrame* rootFrame = mPresShell->GetRootFrame();
if (!rootFrame) {
return NS_ERROR_NOT_AVAILABLE;
}
// Find the frame under point.
AutoWeakFrame ptFrame = nsLayoutUtils::GetFrameForPoint(
RelativeTo{rootFrame}, aPoint, GetHitTestOptions());
if (!ptFrame.GetFrame()) {
return NS_ERROR_FAILURE;
}
nsIFrame* focusableFrame = GetFocusableFrame(ptFrame);
#ifdef DEBUG_FRAME_DUMP
AC_LOG("%s: Found %s under (%d, %d)", __FUNCTION__, ptFrame->ListTag().get(),
aPoint.x, aPoint.y);
AC_LOG("%s: Found %s focusable", __FUNCTION__,
focusableFrame ? focusableFrame->ListTag().get() : "no frame");
#endif
// Get ptInFrame here so that we don't need to check whether rootFrame is
// alive later. Note that if ptFrame is being moved by
// IMEStateManager::NotifyIME() or ChangeFocusToOrClearOldFocus() below,
// something under the original point will be selected, which may not be the
// original text the user wants to select.
nsPoint ptInFrame = aPoint;
nsLayoutUtils::TransformPoint(RelativeTo{rootFrame}, RelativeTo{ptFrame},
ptInFrame);
// Firstly check long press on an empty editable content.
Element* newFocusEditingHost = GetEditingHostForFrame(ptFrame);
if (focusableFrame && newFocusEditingHost &&
!HasNonEmptyTextContent(newFocusEditingHost)) {
ChangeFocusToOrClearOldFocus(focusableFrame);
if (StaticPrefs::
layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
mCarets.GetFirst()->SetAppearance(Appearance::Normal);
}
// We need to update carets to get correct information before dispatching
// CaretStateChangedEvent.
UpdateCarets();
ProvideHapticFeedback();
DispatchCaretStateChangedEvent(CaretChangedReason::Longpressonemptycontent);
return NS_OK;
}
bool selectable = ptFrame->IsSelectable(nullptr);
#ifdef DEBUG_FRAME_DUMP
AC_LOG("%s: %s %s selectable.", __FUNCTION__, ptFrame->ListTag().get(),
selectable ? "is" : "is NOT");
#endif
if (!selectable) {
return NS_ERROR_FAILURE;
}
// Commit the composition string of the old editable focus element (if there
// is any) before changing the focus.
IMEStateManager::NotifyIME(widget::REQUEST_TO_COMMIT_COMPOSITION,
mPresShell->GetPresContext());
if (!ptFrame.IsAlive()) {
// Cannot continue because ptFrame died.
return NS_ERROR_FAILURE;
}
// ptFrame is selectable. Now change the focus.
ChangeFocusToOrClearOldFocus(focusableFrame);
if (!ptFrame.IsAlive()) {
// Cannot continue because ptFrame died.
return NS_ERROR_FAILURE;
}
// If long tap point isn't selectable frame for caret and frame selection
// can find a better frame for caret, we don't select a word.
nsIFrame::ContentOffsets offsets = ptFrame->GetContentOffsetsFromPoint(
ptInFrame,
nsIFrame::SKIP_HIDDEN | nsIFrame::IGNORE_NATIVE_ANONYMOUS_SUBTREE);
if (offsets.content) {
RefPtr<nsFrameSelection> frameSelection = GetFrameSelection();
if (frameSelection) {
nsIFrame* theFrame = SelectionMovementUtils::GetFrameForNodeOffset(
offsets.content, offsets.offset, offsets.associate);
if (theFrame && theFrame != ptFrame) {
SetSelectionDragState(true);
frameSelection->HandleClick(
MOZ_KnownLive(offsets.content) /* bug 1636889 */,
offsets.StartOffset(), offsets.EndOffset(),
nsFrameSelection::FocusMode::kCollapseToNewPoint,
offsets.associate);
SetSelectionDragState(false);
ClearMaintainedSelection();
if (StaticPrefs::
layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
mCarets.GetFirst()->SetAppearance(Appearance::Normal);
}
UpdateCarets();
ProvideHapticFeedback();
DispatchCaretStateChangedEvent(
CaretChangedReason::Longpressonemptycontent);
return NS_OK;
}
}
}
// Then try select a word under point.
nsresult rv = SelectWord(ptFrame, ptInFrame);
UpdateCarets();
ProvideHapticFeedback();
return rv;
}
void AccessibleCaretManager::OnScrollStart() {
AC_LOG("%s", __FUNCTION__);
nsAutoScriptBlocker scriptBlocker;
AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing);
mLayoutFlusher.mAllowFlushing = false;
Maybe<PresShell::AutoAssertNoFlush> assert;
if (mPresShell) {
assert.emplace(*mPresShell);
}
mIsScrollStarted = true;
if (mCarets.HasLogicallyVisibleCaret()) {
// Dispatch the event only if one of the carets is logically visible like in
// HideCaretsAndDispatchCaretStateChangedEvent().
DispatchCaretStateChangedEvent(CaretChangedReason::Scroll);
}
}
void AccessibleCaretManager::OnScrollEnd() {
nsAutoScriptBlocker scriptBlocker;
AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing);
mLayoutFlusher.mAllowFlushing = false;
Maybe<PresShell::AutoAssertNoFlush> assert;
if (mPresShell) {
assert.emplace(*mPresShell);
}
mIsScrollStarted = false;
if (GetCaretMode() == CaretMode::Cursor) {
if (!mCarets.GetFirst()->IsLogicallyVisible()) {
// If the caret is hidden (Appearance::None) due to blur, no
// need to update it.
return;
}
}
// For mouse and keyboard input, we don't want to show the carets.
if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
(mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_MOUSE ||
mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_KEYBOARD)) {
AC_LOG("%s: HideCaretsAndDispatchCaretStateChangedEvent()", __FUNCTION__);
HideCaretsAndDispatchCaretStateChangedEvent();
return;
}
AC_LOG("%s: UpdateCarets()", __FUNCTION__);
UpdateCarets();
}
void AccessibleCaretManager::OnScrollPositionChanged() {
nsAutoScriptBlocker scriptBlocker;
AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing);
mLayoutFlusher.mAllowFlushing = false;
Maybe<PresShell::AutoAssertNoFlush> assert;
if (mPresShell) {
assert.emplace(*mPresShell);
}
if (mCarets.HasLogicallyVisibleCaret()) {
if (mIsScrollStarted) {
// We don't want extra CaretStateChangedEvents dispatched when user is
// scrolling the page.
AC_LOG("%s: UpdateCarets(RespectOldAppearance | DispatchNoEvent)",
__FUNCTION__);
UpdateCarets({UpdateCaretsHint::RespectOldAppearance,
UpdateCaretsHint::DispatchNoEvent});
} else {
AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__);
UpdateCarets(UpdateCaretsHint::RespectOldAppearance);
}
}
}
void AccessibleCaretManager::OnReflow() {
nsAutoScriptBlocker scriptBlocker;
AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing);
mLayoutFlusher.mAllowFlushing = false;
Maybe<PresShell::AutoAssertNoFlush> assert;
if (mPresShell) {
assert.emplace(*mPresShell);
}
if (mCarets.HasLogicallyVisibleCaret()) {
AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__);
UpdateCarets(UpdateCaretsHint::RespectOldAppearance);
}
}
void AccessibleCaretManager::OnBlur() {
AC_LOG("%s: HideCaretsAndDispatchCaretStateChangedEvent()", __FUNCTION__);
HideCaretsAndDispatchCaretStateChangedEvent();
}
void AccessibleCaretManager::OnKeyboardEvent() {
if (GetCaretMode() == CaretMode::Cursor) {
AC_LOG("%s: HideCaretsAndDispatchCaretStateChangedEvent()", __FUNCTION__);
HideCaretsAndDispatchCaretStateChangedEvent();
}
}
void AccessibleCaretManager::SetLastInputSource(uint16_t aInputSource) {
mLastInputSource = aInputSource;
}
bool AccessibleCaretManager::ShouldDisableApz() const {
return mDesiredAsyncPanZoomState.Get() ==
DesiredAsyncPanZoomState::Value::Disabled;
}
Selection* AccessibleCaretManager::GetSelection() const {
RefPtr<nsFrameSelection> fs = GetFrameSelection();
if (!fs) {
return nullptr;
}
return fs->GetSelection(SelectionType::eNormal);
}
already_AddRefed<nsFrameSelection> AccessibleCaretManager::GetFrameSelection()
const {
if (!mPresShell) {
return nullptr;
}
// Prevent us from touching the nsFrameSelection associated with other
// PresShell.
RefPtr<nsFrameSelection> fs = mPresShell->GetLastFocusedFrameSelection();
if (!fs || fs->GetPresShell() != mPresShell) {
return nullptr;
}
return fs.forget();
}
nsAutoString AccessibleCaretManager::StringifiedSelection() const {
nsAutoString str;
RefPtr<Selection> selection = GetSelection();
if (selection) {
selection->Stringify(str, mLayoutFlusher.mAllowFlushing
? Selection::FlushFrames::Yes
: Selection::FlushFrames::No);
}
return str;
}
// static
Element* AccessibleCaretManager::GetEditingHostForFrame(
const nsIFrame* aFrame) {
if (!aFrame) {
return nullptr;
}
auto content = aFrame->GetContent();
if (!content) {
return nullptr;
}
return content->GetEditingHost();
}
AccessibleCaretManager::CaretMode AccessibleCaretManager::GetCaretMode() const {
const Selection* selection = GetSelection();
if (!selection) {
return CaretMode::None;
}
const uint32_t rangeCount = selection->RangeCount();
if (rangeCount <= 0) {
return CaretMode::None;
}
const nsFocusManager* fm = nsFocusManager::GetFocusManager();
MOZ_ASSERT(fm);
if (fm->GetFocusedWindow() != mPresShell->GetDocument()->GetWindow()) {
// Hide carets if the window is not focused.
return CaretMode::None;
}
if (selection->IsCollapsed()) {
return CaretMode::Cursor;
}
return CaretMode::Selection;
}
nsIFrame* AccessibleCaretManager::GetFocusableFrame(nsIFrame* aFrame) const {
// This implementation is similar to EventStateManager::PostHandleEvent().
// Look for the nearest enclosing focusable frame.
nsIFrame* focusableFrame = aFrame;
while (focusableFrame) {
if (focusableFrame->IsFocusable(/* aWithMouse = */ true)) {
break;
}
focusableFrame = focusableFrame->GetParent();
}
return focusableFrame;
}
void AccessibleCaretManager::ChangeFocusToOrClearOldFocus(
nsIFrame* aFrame) const {
RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager();
MOZ_ASSERT(fm);
if (aFrame) {
nsIContent* focusableContent = aFrame->GetContent();
MOZ_ASSERT(focusableContent, "Focusable frame must have content!");
RefPtr<Element> focusableElement = Element::FromNode(focusableContent);
fm->SetFocus(focusableElement, nsIFocusManager::FLAG_BYLONGPRESS);
} else if (nsCOMPtr<nsPIDOMWindowOuter> win =
mPresShell->GetDocument()->GetWindow()) {
fm->ClearFocus(win);
fm->SetFocusedWindow(win);
}
}
nsresult AccessibleCaretManager::SelectWord(nsIFrame* aFrame,
const nsPoint& aPoint) const {
AC_LOGV("%s", __FUNCTION__);
SetSelectionDragState(true);
const RefPtr<nsPresContext> pinnedPresContext{mPresShell->GetPresContext()};
nsresult rs = aFrame->SelectByTypeAtPoint(pinnedPresContext, aPoint,
eSelectWord, eSelectWord, 0);
SetSelectionDragState(false);
ClearMaintainedSelection();
// Smart-select phone numbers if possible.
if (StaticPrefs::layout_accessiblecaret_extend_selection_for_phone_number()) {
SelectMoreIfPhoneNumber();
}
return rs;
}
void AccessibleCaretManager::SetSelectionDragState(bool aState) const {
RefPtr<nsFrameSelection> fs = GetFrameSelection();
if (fs) {
fs->SetDragState(aState);
}
}
bool AccessibleCaretManager::IsPhoneNumber(const nsAString& aCandidate) const {
RefPtr<Document> doc = mPresShell->GetDocument();
nsAutoString phoneNumberRegex(u"(^\\+)?[0-9 ,\\-.\\(\\)*#pw]{1,30}$"_ns);
return nsContentUtils::IsPatternMatching(aCandidate,
std::move(phoneNumberRegex), doc)
.valueOr(false);
}
void AccessibleCaretManager::SelectMoreIfPhoneNumber() const {
if (IsPhoneNumber(StringifiedSelection())) {
SetSelectionDirection(eDirNext);
ExtendPhoneNumberSelection(u"forward"_ns);
SetSelectionDirection(eDirPrevious);
ExtendPhoneNumberSelection(u"backward"_ns);
SetSelectionDirection(eDirNext);
}
}
void AccessibleCaretManager::ExtendPhoneNumberSelection(
const nsAString& aDirection) const {
if (!mPresShell) {
return;
}
// Extend the phone number selection until we find a boundary.
RefPtr<Selection> selection = GetSelection();
while (selection) {
const nsRange* anchorFocusRange = selection->GetAnchorFocusRange();
if (!anchorFocusRange) {
return;
}
// Backup the anchor focus range since both anchor node and focus node might
// be changed after calling Selection::Modify().
RefPtr<nsRange> oldAnchorFocusRange = anchorFocusRange->CloneRange();
// Save current focus node, focus offset and the selected text so that
// we can compare them with the modified ones later.
nsINode* oldFocusNode = selection->GetFocusNode();
uint32_t oldFocusOffset = selection->FocusOffset();
nsAutoString oldSelectedText = StringifiedSelection();
// Extend the selection by one char.
selection->Modify(u"extend"_ns, aDirection, u"character"_ns,
IgnoreErrors());
if (IsTerminated() == Terminated::Yes) {
return;
}
// If the selection didn't change, (can't extend further), we're done.
if (selection->GetFocusNode() == oldFocusNode &&
selection->FocusOffset() == oldFocusOffset) {
return;
}
// If the changed selection isn't a valid phone number, we're done.
// Also, if the selection was extended to a new block node, the string
// returned by stringify() won't have a new line at the beginning or the
// end of the string. Therefore, if either focus node or offset is
// changed, but selected text is not changed, we're done, too.
nsAutoString selectedText = StringifiedSelection();
if (!IsPhoneNumber(selectedText) || oldSelectedText == selectedText) {
// Backout the undesired selection extend, restore the old anchor focus
// range before exit.
selection->SetAnchorFocusToRange(oldAnchorFocusRange);
return;
}
}
}
void AccessibleCaretManager::SetSelectionDirection(nsDirection aDir) const {
Selection* selection = GetSelection();
if (selection) {
selection->AdjustAnchorFocusForMultiRange(aDir);
}
}
void AccessibleCaretManager::ClearMaintainedSelection() const {
// Selection made by double-clicking for example will maintain the original
// word selection. We should clear it so that we can drag caret freely.
RefPtr<nsFrameSelection> fs = GetFrameSelection();
if (fs) {
fs->MaintainSelection(eSelectNoAmount);
}
}
void AccessibleCaretManager::LayoutFlusher::MaybeFlush(
const PresShell& aPresShell) {
if (mAllowFlushing) {
AutoRestore<bool> flushing(mFlushing);
mFlushing = true;
if (Document* doc = aPresShell.GetDocument()) {
doc->FlushPendingNotifications(FlushType::Layout);
// Don't access the PresShell after flushing, it could've become invalid.
}
}
}
nsIFrame* AccessibleCaretManager::GetFrameForFirstRangeStartOrLastRangeEnd(
nsDirection aDirection, int32_t* aOutOffset, nsIContent** aOutContent,
int32_t* aOutContentOffset) const {
if (!mPresShell) {
return nullptr;
}
MOZ_ASSERT(GetCaretMode() == CaretMode::Selection);
MOZ_ASSERT(aOutOffset, "aOutOffset shouldn't be nullptr!");
const nsRange* range = nullptr;
RefPtr<nsINode> startNode;
RefPtr<nsINode> endNode;
int32_t nodeOffset = 0;
CaretAssociationHint hint;
RefPtr<Selection> selection = GetSelection();
bool findInFirstRangeStart = aDirection == eDirNext;
if (findInFirstRangeStart) {
range = selection->GetRangeAt(0);
startNode = range->GetStartContainer();
endNode = range->GetEndContainer();
nodeOffset = range->StartOffset();
hint = CaretAssociationHint::After;
} else {
MOZ_ASSERT(selection->RangeCount() > 0);
range = selection->GetRangeAt(selection->RangeCount() - 1);
startNode = range->GetEndContainer();
endNode = range->GetStartContainer();
nodeOffset = range->EndOffset();
hint = CaretAssociationHint::Before;
}
nsCOMPtr<nsIContent> startContent = nsIContent::FromNodeOrNull(startNode);
uint32_t outOffset = 0;
nsIFrame* startFrame = SelectionMovementUtils::GetFrameForNodeOffset(
startContent, nodeOffset, hint, &outOffset);
*aOutOffset = static_cast<int32_t>(outOffset);
if (!startFrame) {
ErrorResult err;
RefPtr<TreeWalker> walker = mPresShell->GetDocument()->CreateTreeWalker(
*startNode, dom::NodeFilter_Binding::SHOW_ALL, nullptr, err);
if (!walker) {
return nullptr;
}
startFrame = startContent ? startContent->GetPrimaryFrame() : nullptr;
while (!startFrame && startNode != endNode) {
startNode = findInFirstRangeStart ? walker->NextNode(err)
: walker->PreviousNode(err);
if (!startNode) {
break;
}
startContent = startNode->AsContent();
startFrame = startContent ? startContent->GetPrimaryFrame() : nullptr;
}
// We are walking among the nodes in the content tree, so the node offset
// relative to startNode should be set to 0.
nodeOffset = 0;
*aOutOffset = 0;
}
if (startFrame) {
if (aOutContent) {
startContent.forget(aOutContent);
}
if (aOutContentOffset) {
*aOutContentOffset = nodeOffset;
}
}
return startFrame;
}
bool AccessibleCaretManager::RestrictCaretDraggingOffsets(
nsIFrame::ContentOffsets& aOffsets) {
if (!mPresShell) {
return false;
}
MOZ_ASSERT(GetCaretMode() == CaretMode::Selection);
nsDirection dir =
mActiveCaret == mCarets.GetFirst() ? eDirPrevious : eDirNext;
int32_t offset = 0;
nsCOMPtr<nsIContent> content;
int32_t contentOffset = 0;
nsIFrame* frame = GetFrameForFirstRangeStartOrLastRangeEnd(
dir, &offset, getter_AddRefs(content), &contentOffset);
if (!frame) {
return false;
}
// Compare the active caret's new position (aOffsets) to the inactive caret's
// position.
NS_ASSERTION(contentOffset >= 0, "contentOffset should not be negative");
const Maybe<int32_t> cmpToInactiveCaretPos =
nsContentUtils::ComparePoints_AllowNegativeOffsets(
aOffsets.content, aOffsets.StartOffset(), content, contentOffset);
if (NS_WARN_IF(!cmpToInactiveCaretPos)) {
// Potentially handle this properly when Selection across Shadow DOM
// boundary is implemented
return false;
}
// Move one character (in the direction of dir) from the inactive caret's
// position. This is the limit for the active caret's new position.
PeekOffsetStruct limit(
eSelectCluster, dir, offset, nsPoint(0, 0),
{PeekOffsetOption::JumpLines, PeekOffsetOption::StopAtScroller});
nsresult rv = frame->PeekOffset(&limit);
if (NS_FAILED(rv)) {
limit.mResultContent = content;
limit.mContentOffset = contentOffset;
}
// Compare the active caret's new position (aOffsets) to the limit.
NS_ASSERTION(limit.mContentOffset >= 0,
"limit.mContentOffset should not be negative");
const Maybe<int32_t> cmpToLimit =
nsContentUtils::ComparePoints_AllowNegativeOffsets(
aOffsets.content, aOffsets.StartOffset(), limit.mResultContent,
limit.mContentOffset);
if (NS_WARN_IF(!cmpToLimit)) {
// Potentially handle this properly when Selection across Shadow DOM
// boundary is implemented
return false;
}
auto SetOffsetsToLimit = [&aOffsets, &limit]() {
aOffsets.content = limit.mResultContent;
aOffsets.offset = limit.mContentOffset;
aOffsets.secondaryOffset = limit.mContentOffset;
};
if (!StaticPrefs::
layout_accessiblecaret_allow_dragging_across_other_caret()) {
if ((mActiveCaret == mCarets.GetFirst() && *cmpToLimit == 1) ||
(mActiveCaret == mCarets.GetSecond() && *cmpToLimit == -1)) {
// The active caret's position is past the limit, which we don't allow
// here. So set it to the limit, resulting in one character being
// selected.
SetOffsetsToLimit();
}
} else {
switch (*cmpToInactiveCaretPos) {
case 0:
// The active caret's position is the same as the position of the
// inactive caret. So set it to the limit to prevent the selection from
// being collapsed, resulting in one character being selected.
SetOffsetsToLimit();
break;
case 1:
if (mActiveCaret == mCarets.GetFirst()) {
// First caret was moved across the second caret. After making change
// to the selection, the user will drag the second caret.
mActiveCaret = mCarets.GetSecond();
}
break;
case -1:
if (mActiveCaret == mCarets.GetSecond()) {
// Second caret was moved across the first caret. After making change
// to the selection, the user will drag the first caret.
mActiveCaret = mCarets.GetFirst();
}
break;
}
}
return true;
}
bool AccessibleCaretManager::CompareTreePosition(nsIFrame* aStartFrame,
nsIFrame* aEndFrame) const {
return (aStartFrame && aEndFrame &&
nsLayoutUtils::CompareTreePosition(aStartFrame, aEndFrame) <= 0);
}
nsresult AccessibleCaretManager::DragCaretInternal(const nsPoint& aPoint) {
MOZ_ASSERT(mPresShell);
nsIFrame* rootFrame = mPresShell->GetRootFrame();
MOZ_ASSERT(rootFrame, "We need root frame to compute caret dragging!");
nsPoint point = AdjustDragBoundary(
nsPoint(aPoint.x, aPoint.y + mOffsetYToCaretLogicalPosition));
// Find out which content we point to
nsIFrame* ptFrame = nsLayoutUtils::GetFrameForPoint(
RelativeTo{rootFrame}, point, GetHitTestOptions());
if (!ptFrame) {
return NS_ERROR_FAILURE;
}
RefPtr<nsFrameSelection> fs = GetFrameSelection();
MOZ_ASSERT(fs);
nsresult result;
nsIFrame* newFrame = nullptr;
nsPoint newPoint;
nsPoint ptInFrame = point;
nsLayoutUtils::TransformPoint(RelativeTo{rootFrame}, RelativeTo{ptFrame},
ptInFrame);
result = fs->ConstrainFrameAndPointToAnchorSubtree(ptFrame, ptInFrame,
&newFrame, newPoint);
if (NS_FAILED(result) || !newFrame) {
return NS_ERROR_FAILURE;
}
if (!newFrame->IsSelectable(nullptr)) {
return NS_ERROR_FAILURE;
}
nsIFrame::ContentOffsets offsets = newFrame->GetContentOffsetsFromPoint(
newPoint, nsIFrame::IGNORE_NATIVE_ANONYMOUS_SUBTREE);
if (offsets.IsNull()) {
return NS_ERROR_FAILURE;
}
if (GetCaretMode() == CaretMode::Selection &&
!RestrictCaretDraggingOffsets(offsets)) {
return NS_ERROR_FAILURE;
}
ClearMaintainedSelection();
const nsFrameSelection::FocusMode focusMode =
(GetCaretMode() == CaretMode::Selection)
? nsFrameSelection::FocusMode::kExtendSelection
: nsFrameSelection::FocusMode::kCollapseToNewPoint;
fs->HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */,
offsets.StartOffset(), offsets.EndOffset(), focusMode,
offsets.associate);
return NS_OK;
}
// static
nsRect AccessibleCaretManager::GetAllChildFrameRectsUnion(nsIFrame* aFrame) {
nsRect unionRect;
// Drill through scroll frames, we don't want to include scrollbar child
// frames below.
for (nsIFrame* frame = aFrame->GetContentInsertionFrame(); frame;
frame = frame->GetNextContinuation()) {
nsRect frameRect;
for (const auto& childList : frame->ChildLists()) {
// Loop all children to union their scrollable overflow rect.
for (nsIFrame* child : childList.mList) {
nsRect childRect = child->ScrollableOverflowRectRelativeToSelf();
nsLayoutUtils::TransformRect(child, frame, childRect);
// A TextFrame containing only '\n' has positive height and width 0, or
// positive width and height 0 if it's vertical. Need to use UnionEdges
// to add its rect. BRFrame rect should be non-empty.
if (childRect.IsEmpty()) {
frameRect = frameRect.UnionEdges(childRect);
} else {
frameRect = frameRect.Union(childRect);
}
}
}
MOZ_ASSERT(!frameRect.IsEmpty(),
"Editable frames should have at least one BRFrame child to make "
"frameRect non-empty!");
if (frame != aFrame) {
nsLayoutUtils::TransformRect(frame, aFrame, frameRect);
}
unionRect = unionRect.Union(frameRect);
}
return unionRect;
}
nsPoint AccessibleCaretManager::AdjustDragBoundary(
const nsPoint& aPoint) const {
nsPoint adjustedPoint = aPoint;
auto frameData =
nsCaret::GetFrameAndOffset(nsCaret::CaretPositionFor(GetSelection()));
Element* editingHost = GetEditingHostForFrame(frameData.mFrame);
if (editingHost) {
nsIFrame* editingHostFrame = editingHost->GetPrimaryFrame();
if (editingHostFrame) {
nsRect boundary =
AccessibleCaretManager::GetAllChildFrameRectsUnion(editingHostFrame);
nsLayoutUtils::TransformRect(editingHostFrame, mPresShell->GetRootFrame(),
boundary);
// Shrink the rect to make sure we never hit the boundary.
boundary.Deflate(kBoundaryAppUnits);
adjustedPoint = boundary.ClampPoint(adjustedPoint);
}
}
if (GetCaretMode() == CaretMode::Selection &&
!StaticPrefs::
layout_accessiblecaret_allow_dragging_across_other_caret()) {
// Bug 1068474: Adjust the Y-coordinate so that the carets won't be in tilt
// mode when a caret is being dragged surpass the other caret.
//
// For example, when dragging the second caret, the horizontal boundary
// (lower bound) of its Y-coordinate is the logical position of the first
// caret. Likewise, when dragging the first caret, the horizontal boundary
// (upper bound) of its Y-coordinate is the logical position of the second
// caret.
if (mActiveCaret == mCarets.GetFirst()) {
nscoord dragDownBoundaryY = mCarets.GetSecond()->LogicalPosition().y;
if (dragDownBoundaryY > 0 && adjustedPoint.y > dragDownBoundaryY) {
adjustedPoint.y = dragDownBoundaryY;
}
} else {
nscoord dragUpBoundaryY = mCarets.GetFirst()->LogicalPosition().y;
if (adjustedPoint.y < dragUpBoundaryY) {
adjustedPoint.y = dragUpBoundaryY;
}
}
}
return adjustedPoint;
}
void AccessibleCaretManager::StartSelectionAutoScrollTimer(
const nsPoint& aPoint) const {
Selection* selection = GetSelection();
MOZ_ASSERT(selection);
nsIFrame* anchorFrame = selection->GetPrimaryFrameForAnchorNode();
if (!anchorFrame) {
return;
}
nsIScrollableFrame* scrollFrame = nsLayoutUtils::GetNearestScrollableFrame(
anchorFrame, nsLayoutUtils::SCROLLABLE_SAME_DOC |
nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
if (!scrollFrame) {
return;
}
nsIFrame* capturingFrame = scrollFrame->GetScrolledFrame();
if (!capturingFrame) {
return;
}
nsIFrame* rootFrame = mPresShell->GetRootFrame();
MOZ_ASSERT(rootFrame);
nsPoint ptInScrolled = aPoint;
nsLayoutUtils::TransformPoint(RelativeTo{rootFrame},
RelativeTo{capturingFrame}, ptInScrolled);
RefPtr<nsFrameSelection> fs = GetFrameSelection();
MOZ_ASSERT(fs);
fs->StartAutoScrollTimer(capturingFrame, ptInScrolled, kAutoScrollTimerDelay);
}
void AccessibleCaretManager::StopSelectionAutoScrollTimer() const {
RefPtr<nsFrameSelection> fs = GetFrameSelection();
MOZ_ASSERT(fs);
fs->StopAutoScrollTimer();
}
void AccessibleCaretManager::DispatchCaretStateChangedEvent(
CaretChangedReason aReason, const nsPoint* aPoint) {
if (MaybeFlushLayout() == Terminated::Yes) {
return;
}
const Selection* sel = GetSelection();
if (!sel) {
return;
}
Document* doc = mPresShell->GetDocument();
MOZ_ASSERT(doc);
CaretStateChangedEventInit init;
init.mBubbles = true;
const nsRange* range = sel->GetAnchorFocusRange();
nsINode* commonAncestorNode = nullptr;
if (range) {
commonAncestorNode = range->GetClosestCommonInclusiveAncestor();
}
if (!commonAncestorNode) {
commonAncestorNode = sel->GetFrameSelection()->GetAncestorLimiter();
}
RefPtr<DOMRect> domRect = new DOMRect(ToSupports(doc));
nsRect rect = nsLayoutUtils::GetSelectionBoundingRect(sel);
nsIFrame* commonAncestorFrame = nullptr;
nsIFrame* rootFrame = mPresShell->GetRootFrame();
if (commonAncestorNode && commonAncestorNode->IsContent()) {
commonAncestorFrame = commonAncestorNode->AsContent()->GetPrimaryFrame();
}
if (commonAncestorFrame && rootFrame) {
nsLayoutUtils::TransformRect(rootFrame, commonAncestorFrame, rect);
nsRect clampedRect =
nsLayoutUtils::ClampRectToScrollFrames(commonAncestorFrame, rect);
nsLayoutUtils::TransformRect(commonAncestorFrame, rootFrame, clampedRect);
rect = clampedRect;
init.mSelectionVisible = !clampedRect.IsEmpty();
} else {
init.mSelectionVisible = true;
}
domRect->SetLayoutRect(rect);
// Send isEditable info w/ event detail. This info can help determine
// whether to show cut command on selection dialog or not.
init.mSelectionEditable = GetEditingHostForFrame(commonAncestorFrame);
init.mBoundingClientRect = domRect;
init.mReason = aReason;
init.mCollapsed = sel->IsCollapsed();
init.mCaretVisible = mCarets.HasLogicallyVisibleCaret();
init.mCaretVisuallyVisible = mCarets.HasVisuallyVisibleCaret();
init.mSelectedTextContent = StringifiedSelection();
if (aPoint) {
CSSIntPoint pt = CSSPixel::FromAppUnitsRounded(*aPoint);
init.mClientX = pt.x;
init.mClientY = pt.y;
}
RefPtr<CaretStateChangedEvent> event = CaretStateChangedEvent::Constructor(
doc, u"mozcaretstatechanged"_ns, init);
event->SetTrusted(true);
AC_LOG("%s: reason %" PRIu32 ", collapsed %d, caretVisible %" PRIu32,
__FUNCTION__, static_cast<uint32_t>(init.mReason), init.mCollapsed,
static_cast<uint32_t>(init.mCaretVisible));
(new AsyncEventDispatcher(doc, event.forget(), ChromeOnlyDispatch::eYes))
->PostDOMEvent();
}
AccessibleCaretManager::Carets::Carets(UniquePtr<AccessibleCaret> aFirst,
UniquePtr<AccessibleCaret> aSecond)
: mFirst{std::move(aFirst)}, mSecond{std::move(aSecond)} {}
} // namespace mozilla