summaryrefslogtreecommitdiffstats
path: root/layout/base/AccessibleCaretManager.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--layout/base/AccessibleCaretManager.cpp1502
1 files changed, 1502 insertions, 0 deletions
diff --git a/layout/base/AccessibleCaretManager.cpp b/layout/base/AccessibleCaretManager.cpp
new file mode 100644
index 0000000000..bdf0ebaf92
--- /dev/null
+++ b/layout/base/AccessibleCaretManager.cpp
@@ -0,0 +1,1502 @@
+/* -*- 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/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/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;
+ }
+
+ int32_t offset = 0;
+ nsIFrame* frame =
+ nsCaret::GetFrameAndOffset(GetSelection(), nullptr, 0, &offset);
+
+ if (!frame) {
+ return false;
+ }
+
+ if (!GetEditingHostForFrame(frame)) {
+ return false;
+ }
+
+ if (aOutFrame) {
+ *aOutFrame = frame;
+ }
+
+ if (aOutOffset) {
+ *aOutOffset = offset;
+ }
+
+ 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()) {
+ 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.
+ // See https://webcompat.com/issues/15953
+ nsIFrame::ContentOffsets offsets =
+ ptFrame->GetContentOffsetsFromPoint(ptInFrame, nsIFrame::SKIP_HIDDEN);
+ if (offsets.content) {
+ RefPtr<nsFrameSelection> frameSelection = GetFrameSelection();
+ if (frameSelection) {
+ int32_t offset;
+ nsIFrame* theFrame = nsFrameSelection::GetFrameForNodeOffset(
+ offsets.content, offsets.offset, offsets.associate, &offset);
+ 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__);
+
+ 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() {
+ 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 input we don't want to show the carets.
+ if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
+ mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_MOUSE) {
+ AC_LOG("%s: HideCaretsAndDispatchCaretStateChangedEvent()", __FUNCTION__);
+ HideCaretsAndDispatchCaretStateChangedEvent();
+ return;
+ }
+
+ AC_LOG("%s: UpdateCarets()", __FUNCTION__);
+ UpdateCarets();
+}
+
+void AccessibleCaretManager::OnScrollPositionChanged() {
+ 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() {
+ 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::OnFrameReconstruction() {
+ mCarets.GetFirst()->EnsureApzAware();
+ mCarets.GetSecond()->EnsureApzAware();
+}
+
+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(nsAString& aCandidate) const {
+ RefPtr<Document> doc = mPresShell->GetDocument();
+ nsAutoString phoneNumberRegex(u"(^\\+)?[0-9 ,\\-.()*#pw]{1,30}$"_ns);
+ return nsContentUtils::IsPatternMatching(aCandidate, phoneNumberRegex, doc)
+ .valueOr(false);
+}
+
+void AccessibleCaretManager::SelectMoreIfPhoneNumber() const {
+ nsAutoString selectedText = StringifiedSelection();
+
+ if (IsPhoneNumber(selectedText)) {
+ 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 = CARET_ASSOCIATE_AFTER;
+ } else {
+ MOZ_ASSERT(selection->RangeCount() > 0);
+ range = selection->GetRangeAt(selection->RangeCount() - 1);
+ startNode = range->GetEndContainer();
+ endNode = range->GetStartContainer();
+ nodeOffset = range->EndOffset();
+ hint = CARET_ASSOCIATE_BEFORE;
+ }
+
+ nsCOMPtr<nsIContent> startContent = do_QueryInterface(startNode);
+ nsIFrame* startFrame = nsFrameSelection::GetFrameForNodeOffset(
+ startContent, nodeOffset, hint, aOutOffset);
+
+ 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
+ // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
+ 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.
+ nsPeekOffsetStruct limit(eSelectCluster, dir, offset, nsPoint(0, 0), true,
+ true, false, false, false);
+ 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
+ // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
+ 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);
+ 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;
+
+ int32_t focusOffset = 0;
+ nsIFrame* focusFrame =
+ nsCaret::GetFrameAndOffset(GetSelection(), nullptr, 0, &focusOffset);
+ Element* editingHost = GetEditingHostForFrame(focusFrame);
+
+ 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 =
+ commonAncestorFrame && 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);
+ event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = 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))->PostDOMEvent();
+}
+
+AccessibleCaretManager::Carets::Carets(UniquePtr<AccessibleCaret> aFirst,
+ UniquePtr<AccessibleCaret> aSecond)
+ : mFirst{std::move(aFirst)}, mSecond{std::move(aSecond)} {}
+
+} // namespace mozilla