Bug 1879449 [wpt PR 44489] - [wptrunner] Add `infrastructure/expected-fail/` test...
[gecko.git] / layout / base / AccessibleCaretManager.cpp
blob17597d287dd2a33c097a69daae1c71be3d336d8c
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "AccessibleCaretManager.h"
9 #include <utility>
11 #include "AccessibleCaret.h"
12 #include "AccessibleCaretEventHub.h"
13 #include "AccessibleCaretLogger.h"
14 #include "mozilla/AsyncEventDispatcher.h"
15 #include "mozilla/AutoRestore.h"
16 #include "mozilla/CaretAssociationHint.h"
17 #include "mozilla/dom/Element.h"
18 #include "mozilla/dom/MouseEventBinding.h"
19 #include "mozilla/dom/NodeFilterBinding.h"
20 #include "mozilla/dom/Selection.h"
21 #include "mozilla/dom/TreeWalker.h"
22 #include "mozilla/IMEStateManager.h"
23 #include "mozilla/IntegerPrintfMacros.h"
24 #include "mozilla/PresShell.h"
25 #include "mozilla/SelectionMovementUtils.h"
26 #include "mozilla/StaticAnalysisFunctions.h"
27 #include "mozilla/StaticPrefs_layout.h"
28 #include "nsCaret.h"
29 #include "nsContainerFrame.h"
30 #include "nsContentUtils.h"
31 #include "nsDebug.h"
32 #include "nsFocusManager.h"
33 #include "nsIFrame.h"
34 #include "nsFrameSelection.h"
35 #include "nsGenericHTMLElement.h"
36 #include "nsIHapticFeedback.h"
37 #include "nsIScrollableFrame.h"
38 #include "nsLayoutUtils.h"
39 #include "nsServiceManagerUtils.h"
41 namespace mozilla {
43 #undef AC_LOG
44 #define AC_LOG(message, ...) \
45 AC_LOG_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
47 #undef AC_LOGV
48 #define AC_LOGV(message, ...) \
49 AC_LOGV_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
51 using namespace dom;
52 using Appearance = AccessibleCaret::Appearance;
53 using PositionChangedResult = AccessibleCaret::PositionChangedResult;
55 #define AC_PROCESS_ENUM_TO_STREAM(e) \
56 case (e): \
57 aStream << #e; \
58 break;
59 std::ostream& operator<<(std::ostream& aStream,
60 const AccessibleCaretManager::CaretMode& aCaretMode) {
61 using CaretMode = AccessibleCaretManager::CaretMode;
62 switch (aCaretMode) {
63 AC_PROCESS_ENUM_TO_STREAM(CaretMode::None);
64 AC_PROCESS_ENUM_TO_STREAM(CaretMode::Cursor);
65 AC_PROCESS_ENUM_TO_STREAM(CaretMode::Selection);
67 return aStream;
70 std::ostream& operator<<(
71 std::ostream& aStream,
72 const AccessibleCaretManager::UpdateCaretsHint& aHint) {
73 using UpdateCaretsHint = AccessibleCaretManager::UpdateCaretsHint;
74 switch (aHint) {
75 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::Default);
76 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::RespectOldAppearance);
77 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::DispatchNoEvent);
79 return aStream;
81 #undef AC_PROCESS_ENUM_TO_STREAM
83 AccessibleCaretManager::AccessibleCaretManager(PresShell* aPresShell)
84 : AccessibleCaretManager{
85 aPresShell,
86 Carets{aPresShell ? MakeUnique<AccessibleCaret>(aPresShell) : nullptr,
87 aPresShell ? MakeUnique<AccessibleCaret>(aPresShell)
88 : nullptr}} {}
90 AccessibleCaretManager::AccessibleCaretManager(PresShell* aPresShell,
91 Carets aCarets)
92 : mPresShell{aPresShell}, mCarets{std::move(aCarets)} {}
94 AccessibleCaretManager::LayoutFlusher::~LayoutFlusher() {
95 MOZ_RELEASE_ASSERT(!mFlushing, "Going away in MaybeFlush? Bad!");
98 void AccessibleCaretManager::Terminate() {
99 mCarets.Terminate();
100 mActiveCaret = nullptr;
101 mPresShell = nullptr;
104 nsresult AccessibleCaretManager::OnSelectionChanged(Document* aDoc,
105 Selection* aSel,
106 int16_t aReason) {
107 Selection* selection = GetSelection();
108 AC_LOG("%s: aSel: %p, GetSelection(): %p, aReason: %d", __FUNCTION__, aSel,
109 selection, aReason);
110 if (aSel != selection) {
111 return NS_OK;
114 // eSetSelection events from the Fennec widget IME can be generated
115 // by autoSuggest / autoCorrect composition changes, or by TYPE_REPLACE_TEXT
116 // actions, either positioning cursor for text insert, or selecting
117 // text-to-be-replaced. None should affect AccessibleCaret visibility.
118 if (aReason & nsISelectionListener::IME_REASON) {
119 return NS_OK;
122 // Move the cursor by JavaScript or unknown internal call.
123 if (aReason == nsISelectionListener::NO_REASON ||
124 aReason == nsISelectionListener::JS_REASON) {
125 auto mode = static_cast<ScriptUpdateMode>(
126 StaticPrefs::layout_accessiblecaret_script_change_update_mode());
127 if (mode == kScriptAlwaysShow ||
128 (mode == kScriptUpdateVisible && mCarets.HasLogicallyVisibleCaret())) {
129 UpdateCarets();
130 return NS_OK;
132 // Default for NO_REASON is to make hidden.
133 HideCaretsAndDispatchCaretStateChangedEvent();
134 return NS_OK;
137 // Move cursor by keyboard.
138 if (aReason & nsISelectionListener::KEYPRESS_REASON) {
139 HideCaretsAndDispatchCaretStateChangedEvent();
140 return NS_OK;
143 // OnBlur() might be called between mouse down and mouse up, so we hide carets
144 // upon mouse down anyway, and update carets upon mouse up.
145 if (aReason & nsISelectionListener::MOUSEDOWN_REASON) {
146 HideCaretsAndDispatchCaretStateChangedEvent();
147 return NS_OK;
150 // Range will collapse after cutting or copying text.
151 if (aReason & (nsISelectionListener::COLLAPSETOSTART_REASON |
152 nsISelectionListener::COLLAPSETOEND_REASON)) {
153 HideCaretsAndDispatchCaretStateChangedEvent();
154 return NS_OK;
157 // For mouse input we don't want to show the carets.
158 if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
159 mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_MOUSE) {
160 HideCaretsAndDispatchCaretStateChangedEvent();
161 return NS_OK;
164 // When we want to hide the carets for mouse input, hide them for select
165 // all action fired by keyboard as well.
166 if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
167 mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_KEYBOARD &&
168 (aReason & nsISelectionListener::SELECTALL_REASON)) {
169 HideCaretsAndDispatchCaretStateChangedEvent();
170 return NS_OK;
173 UpdateCarets();
174 return NS_OK;
177 void AccessibleCaretManager::HideCaretsAndDispatchCaretStateChangedEvent() {
178 if (mCarets.HasLogicallyVisibleCaret()) {
179 AC_LOG("%s", __FUNCTION__);
180 mCarets.GetFirst()->SetAppearance(Appearance::None);
181 mCarets.GetSecond()->SetAppearance(Appearance::None);
182 mIsCaretPositionChanged = false;
183 DispatchCaretStateChangedEvent(CaretChangedReason::Visibilitychange);
187 auto AccessibleCaretManager::MaybeFlushLayout() -> Terminated {
188 if (mPresShell) {
189 // `MaybeFlush` doesn't access the PresShell after flushing, so it's OK to
190 // mark it as live.
191 mLayoutFlusher.MaybeFlush(MOZ_KnownLive(*mPresShell));
194 return IsTerminated();
197 void AccessibleCaretManager::UpdateCarets(const UpdateCaretsHintSet& aHint) {
198 if (MaybeFlushLayout() == Terminated::Yes) {
199 return;
202 mLastUpdateCaretMode = GetCaretMode();
204 switch (mLastUpdateCaretMode) {
205 case CaretMode::None:
206 HideCaretsAndDispatchCaretStateChangedEvent();
207 break;
208 case CaretMode::Cursor:
209 UpdateCaretsForCursorMode(aHint);
210 break;
211 case CaretMode::Selection:
212 UpdateCaretsForSelectionMode(aHint);
213 break;
216 mDesiredAsyncPanZoomState.Update(*this);
219 bool AccessibleCaretManager::IsCaretDisplayableInCursorMode(
220 nsIFrame** aOutFrame, int32_t* aOutOffset) const {
221 RefPtr<nsCaret> caret = mPresShell->GetCaret();
222 if (!caret || !caret->IsVisible()) {
223 return false;
226 int32_t offset = 0;
227 nsIFrame* frame =
228 nsCaret::GetFrameAndOffset(GetSelection(), nullptr, 0, &offset);
230 if (!frame) {
231 return false;
234 if (!GetEditingHostForFrame(frame)) {
235 return false;
238 if (aOutFrame) {
239 *aOutFrame = frame;
242 if (aOutOffset) {
243 *aOutOffset = offset;
246 return true;
249 bool AccessibleCaretManager::HasNonEmptyTextContent(nsINode* aNode) const {
250 return nsContentUtils::HasNonEmptyTextContent(
251 aNode, nsContentUtils::eRecurseIntoChildren);
254 void AccessibleCaretManager::UpdateCaretsForCursorMode(
255 const UpdateCaretsHintSet& aHints) {
256 AC_LOG("%s, selection: %p", __FUNCTION__, GetSelection());
258 int32_t offset = 0;
259 nsIFrame* frame = nullptr;
260 if (!IsCaretDisplayableInCursorMode(&frame, &offset)) {
261 HideCaretsAndDispatchCaretStateChangedEvent();
262 return;
265 PositionChangedResult result = mCarets.GetFirst()->SetPosition(frame, offset);
267 switch (result) {
268 case PositionChangedResult::NotChanged:
269 case PositionChangedResult::Position:
270 case PositionChangedResult::Zoom:
271 if (!aHints.contains(UpdateCaretsHint::RespectOldAppearance)) {
272 if (HasNonEmptyTextContent(GetEditingHostForFrame(frame))) {
273 mCarets.GetFirst()->SetAppearance(Appearance::Normal);
274 } else if (
275 StaticPrefs::
276 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
277 if (mCarets.GetFirst()->IsLogicallyVisible()) {
278 // Possible cases are: 1) SelectWordOrShortcut() sets the
279 // appearance to Normal. 2) When the caret is out of viewport and
280 // now scrolling into viewport, it has appearance NormalNotShown.
281 mCarets.GetFirst()->SetAppearance(Appearance::Normal);
282 } else {
283 // Possible cases are: a) Single tap on current empty content;
284 // OnSelectionChanged() sets the appearance to None due to
285 // MOUSEDOWN_REASON. b) Single tap on other empty content;
286 // OnBlur() sets the appearance to None.
288 // Do nothing to make the appearance remains None so that it can
289 // be distinguished from case 2). Also do not set the appearance
290 // to NormalNotShown here like the default update behavior.
292 } else {
293 mCarets.GetFirst()->SetAppearance(Appearance::NormalNotShown);
296 break;
298 case PositionChangedResult::Invisible:
299 mCarets.GetFirst()->SetAppearance(Appearance::NormalNotShown);
300 break;
303 mCarets.GetSecond()->SetAppearance(Appearance::None);
305 mIsCaretPositionChanged = (result == PositionChangedResult::Position);
307 if (!aHints.contains(UpdateCaretsHint::DispatchNoEvent) && !mActiveCaret) {
308 DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition);
312 void AccessibleCaretManager::UpdateCaretsForSelectionMode(
313 const UpdateCaretsHintSet& aHints) {
314 AC_LOG("%s: selection: %p", __FUNCTION__, GetSelection());
316 int32_t startOffset = 0;
317 nsIFrame* startFrame =
318 GetFrameForFirstRangeStartOrLastRangeEnd(eDirNext, &startOffset);
320 int32_t endOffset = 0;
321 nsIFrame* endFrame =
322 GetFrameForFirstRangeStartOrLastRangeEnd(eDirPrevious, &endOffset);
324 if (!CompareTreePosition(startFrame, endFrame)) {
325 // XXX: Do we really have to hide carets if this condition isn't satisfied?
326 HideCaretsAndDispatchCaretStateChangedEvent();
327 return;
330 auto updateSingleCaret = [aHints](AccessibleCaret* aCaret, nsIFrame* aFrame,
331 int32_t aOffset) -> PositionChangedResult {
332 PositionChangedResult result = aCaret->SetPosition(aFrame, aOffset);
334 switch (result) {
335 case PositionChangedResult::NotChanged:
336 case PositionChangedResult::Position:
337 case PositionChangedResult::Zoom:
338 if (!aHints.contains(UpdateCaretsHint::RespectOldAppearance)) {
339 aCaret->SetAppearance(Appearance::Normal);
341 break;
343 case PositionChangedResult::Invisible:
344 aCaret->SetAppearance(Appearance::NormalNotShown);
345 break;
347 return result;
350 PositionChangedResult firstCaretResult =
351 updateSingleCaret(mCarets.GetFirst(), startFrame, startOffset);
352 PositionChangedResult secondCaretResult =
353 updateSingleCaret(mCarets.GetSecond(), endFrame, endOffset);
355 mIsCaretPositionChanged =
356 firstCaretResult == PositionChangedResult::Position ||
357 secondCaretResult == PositionChangedResult::Position;
359 if (mIsCaretPositionChanged) {
360 // Flush layout to make the carets intersection correct.
361 if (MaybeFlushLayout() == Terminated::Yes) {
362 return;
366 if (!aHints.contains(UpdateCaretsHint::RespectOldAppearance)) {
367 // Only check for tilt carets when the caller doesn't ask us to preserve
368 // old appearance. Otherwise we might override the appearance set by the
369 // caller.
370 if (StaticPrefs::layout_accessiblecaret_always_tilt()) {
371 UpdateCaretsForAlwaysTilt(startFrame, endFrame);
372 } else {
373 UpdateCaretsForOverlappingTilt();
377 if (!aHints.contains(UpdateCaretsHint::DispatchNoEvent) && !mActiveCaret) {
378 DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition);
382 void AccessibleCaretManager::DesiredAsyncPanZoomState::Update(
383 const AccessibleCaretManager& aAccessibleCaretManager) {
384 if (aAccessibleCaretManager.mActiveCaret) {
385 // No need to disable APZ when dragging the caret.
386 mValue = Value::Enabled;
387 return;
390 if (aAccessibleCaretManager.mIsScrollStarted) {
391 // During scrolling, the caret's position is changed only if it is in a
392 // position:fixed or a "stuck" position:sticky frame subtree.
393 mValue = aAccessibleCaretManager.mIsCaretPositionChanged ? Value::Disabled
394 : Value::Enabled;
395 return;
398 // For other cases, we can only reliably detect whether the caret is in a
399 // position:fixed frame subtree.
400 switch (aAccessibleCaretManager.mLastUpdateCaretMode) {
401 case CaretMode::None:
402 mValue = Value::Enabled;
403 break;
404 case CaretMode::Cursor:
405 mValue =
406 (aAccessibleCaretManager.mCarets.GetFirst()->IsVisuallyVisible() &&
407 aAccessibleCaretManager.mCarets.GetFirst()
408 ->IsInPositionFixedSubtree())
409 ? Value::Disabled
410 : Value::Enabled;
411 break;
412 case CaretMode::Selection:
413 mValue =
414 ((aAccessibleCaretManager.mCarets.GetFirst()->IsVisuallyVisible() &&
415 aAccessibleCaretManager.mCarets.GetFirst()
416 ->IsInPositionFixedSubtree()) ||
417 (aAccessibleCaretManager.mCarets.GetSecond()->IsVisuallyVisible() &&
418 aAccessibleCaretManager.mCarets.GetSecond()
419 ->IsInPositionFixedSubtree()))
420 ? Value::Disabled
421 : Value::Enabled;
422 break;
426 bool AccessibleCaretManager::UpdateCaretsForOverlappingTilt() {
427 if (!mCarets.GetFirst()->IsVisuallyVisible() ||
428 !mCarets.GetSecond()->IsVisuallyVisible()) {
429 return false;
432 if (!mCarets.GetFirst()->Intersects(*mCarets.GetSecond())) {
433 mCarets.GetFirst()->SetAppearance(Appearance::Normal);
434 mCarets.GetSecond()->SetAppearance(Appearance::Normal);
435 return false;
438 if (mCarets.GetFirst()->LogicalPosition().x <=
439 mCarets.GetSecond()->LogicalPosition().x) {
440 mCarets.GetFirst()->SetAppearance(Appearance::Left);
441 mCarets.GetSecond()->SetAppearance(Appearance::Right);
442 } else {
443 mCarets.GetFirst()->SetAppearance(Appearance::Right);
444 mCarets.GetSecond()->SetAppearance(Appearance::Left);
447 return true;
450 void AccessibleCaretManager::UpdateCaretsForAlwaysTilt(
451 const nsIFrame* aStartFrame, const nsIFrame* aEndFrame) {
452 // When a short LTR word in RTL environment is selected, the two carets
453 // tilted inward might be overlapped. Make them tilt outward.
454 if (UpdateCaretsForOverlappingTilt()) {
455 return;
458 if (mCarets.GetFirst()->IsVisuallyVisible()) {
459 auto startFrameWritingMode = aStartFrame->GetWritingMode();
460 mCarets.GetFirst()->SetAppearance(startFrameWritingMode.IsBidiLTR()
461 ? Appearance::Left
462 : Appearance::Right);
464 if (mCarets.GetSecond()->IsVisuallyVisible()) {
465 auto endFrameWritingMode = aEndFrame->GetWritingMode();
466 mCarets.GetSecond()->SetAppearance(
467 endFrameWritingMode.IsBidiLTR() ? Appearance::Right : Appearance::Left);
471 void AccessibleCaretManager::ProvideHapticFeedback() {
472 if (StaticPrefs::layout_accessiblecaret_hapticfeedback()) {
473 if (nsCOMPtr<nsIHapticFeedback> haptic =
474 do_GetService("@mozilla.org/widget/hapticfeedback;1")) {
475 haptic->PerformSimpleAction(haptic->LongPress);
480 nsresult AccessibleCaretManager::PressCaret(const nsPoint& aPoint,
481 EventClassID aEventClass) {
482 nsresult rv = NS_ERROR_FAILURE;
484 MOZ_ASSERT(aEventClass == eMouseEventClass || aEventClass == eTouchEventClass,
485 "Unexpected event class!");
487 using TouchArea = AccessibleCaret::TouchArea;
488 TouchArea touchArea =
489 aEventClass == eMouseEventClass ? TouchArea::CaretImage : TouchArea::Full;
491 if (mCarets.GetFirst()->Contains(aPoint, touchArea)) {
492 mActiveCaret = mCarets.GetFirst();
493 SetSelectionDirection(eDirPrevious);
494 } else if (mCarets.GetSecond()->Contains(aPoint, touchArea)) {
495 mActiveCaret = mCarets.GetSecond();
496 SetSelectionDirection(eDirNext);
499 if (mActiveCaret) {
500 mOffsetYToCaretLogicalPosition =
501 mActiveCaret->LogicalPosition().y - aPoint.y;
502 SetSelectionDragState(true);
503 DispatchCaretStateChangedEvent(CaretChangedReason::Presscaret, &aPoint);
504 rv = NS_OK;
507 return rv;
510 nsresult AccessibleCaretManager::DragCaret(const nsPoint& aPoint) {
511 MOZ_ASSERT(mActiveCaret);
512 MOZ_ASSERT(GetCaretMode() != CaretMode::None);
514 if (!mPresShell || !mPresShell->GetRootFrame() || !GetSelection()) {
515 return NS_ERROR_NULL_POINTER;
518 StopSelectionAutoScrollTimer();
519 DragCaretInternal(aPoint);
521 // We want to scroll the page even if we failed to drag the caret.
522 StartSelectionAutoScrollTimer(aPoint);
523 UpdateCarets();
525 if (StaticPrefs::layout_accessiblecaret_magnifier_enabled()) {
526 DispatchCaretStateChangedEvent(CaretChangedReason::Dragcaret, &aPoint);
528 return NS_OK;
531 nsresult AccessibleCaretManager::ReleaseCaret() {
532 MOZ_ASSERT(mActiveCaret);
534 mActiveCaret = nullptr;
535 SetSelectionDragState(false);
536 mDesiredAsyncPanZoomState.Update(*this);
537 DispatchCaretStateChangedEvent(CaretChangedReason::Releasecaret);
538 return NS_OK;
541 nsresult AccessibleCaretManager::TapCaret(const nsPoint& aPoint) {
542 MOZ_ASSERT(GetCaretMode() != CaretMode::None);
544 nsresult rv = NS_ERROR_FAILURE;
546 if (GetCaretMode() == CaretMode::Cursor) {
547 DispatchCaretStateChangedEvent(CaretChangedReason::Taponcaret, &aPoint);
548 rv = NS_OK;
551 return rv;
554 static EnumSet<nsLayoutUtils::FrameForPointOption> GetHitTestOptions() {
555 EnumSet<nsLayoutUtils::FrameForPointOption> options = {
556 nsLayoutUtils::FrameForPointOption::IgnorePaintSuppression,
557 nsLayoutUtils::FrameForPointOption::IgnoreCrossDoc};
558 return options;
561 nsresult AccessibleCaretManager::SelectWordOrShortcut(const nsPoint& aPoint) {
562 // If the long-tap is landing on a pre-existing selection, don't replace
563 // it with a new one. Instead just return and let the context menu pop up
564 // on the pre-existing selection.
565 if (GetCaretMode() == CaretMode::Selection &&
566 GetSelection()->ContainsPoint(aPoint)) {
567 AC_LOG("%s: UpdateCarets() for current selection", __FUNCTION__);
568 UpdateCarets();
569 ProvideHapticFeedback();
570 return NS_OK;
573 if (!mPresShell) {
574 return NS_ERROR_UNEXPECTED;
577 nsIFrame* rootFrame = mPresShell->GetRootFrame();
578 if (!rootFrame) {
579 return NS_ERROR_NOT_AVAILABLE;
582 // Find the frame under point.
583 AutoWeakFrame ptFrame = nsLayoutUtils::GetFrameForPoint(
584 RelativeTo{rootFrame}, aPoint, GetHitTestOptions());
585 if (!ptFrame.GetFrame()) {
586 return NS_ERROR_FAILURE;
589 nsIFrame* focusableFrame = GetFocusableFrame(ptFrame);
591 #ifdef DEBUG_FRAME_DUMP
592 AC_LOG("%s: Found %s under (%d, %d)", __FUNCTION__, ptFrame->ListTag().get(),
593 aPoint.x, aPoint.y);
594 AC_LOG("%s: Found %s focusable", __FUNCTION__,
595 focusableFrame ? focusableFrame->ListTag().get() : "no frame");
596 #endif
598 // Get ptInFrame here so that we don't need to check whether rootFrame is
599 // alive later. Note that if ptFrame is being moved by
600 // IMEStateManager::NotifyIME() or ChangeFocusToOrClearOldFocus() below,
601 // something under the original point will be selected, which may not be the
602 // original text the user wants to select.
603 nsPoint ptInFrame = aPoint;
604 nsLayoutUtils::TransformPoint(RelativeTo{rootFrame}, RelativeTo{ptFrame},
605 ptInFrame);
607 // Firstly check long press on an empty editable content.
608 Element* newFocusEditingHost = GetEditingHostForFrame(ptFrame);
609 if (focusableFrame && newFocusEditingHost &&
610 !HasNonEmptyTextContent(newFocusEditingHost)) {
611 ChangeFocusToOrClearOldFocus(focusableFrame);
613 if (StaticPrefs::
614 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
615 mCarets.GetFirst()->SetAppearance(Appearance::Normal);
617 // We need to update carets to get correct information before dispatching
618 // CaretStateChangedEvent.
619 UpdateCarets();
620 ProvideHapticFeedback();
621 DispatchCaretStateChangedEvent(CaretChangedReason::Longpressonemptycontent);
622 return NS_OK;
625 bool selectable = ptFrame->IsSelectable(nullptr);
627 #ifdef DEBUG_FRAME_DUMP
628 AC_LOG("%s: %s %s selectable.", __FUNCTION__, ptFrame->ListTag().get(),
629 selectable ? "is" : "is NOT");
630 #endif
632 if (!selectable) {
633 return NS_ERROR_FAILURE;
636 // Commit the composition string of the old editable focus element (if there
637 // is any) before changing the focus.
638 IMEStateManager::NotifyIME(widget::REQUEST_TO_COMMIT_COMPOSITION,
639 mPresShell->GetPresContext());
640 if (!ptFrame.IsAlive()) {
641 // Cannot continue because ptFrame died.
642 return NS_ERROR_FAILURE;
645 // ptFrame is selectable. Now change the focus.
646 ChangeFocusToOrClearOldFocus(focusableFrame);
647 if (!ptFrame.IsAlive()) {
648 // Cannot continue because ptFrame died.
649 return NS_ERROR_FAILURE;
652 // If long tap point isn't selectable frame for caret and frame selection
653 // can find a better frame for caret, we don't select a word.
654 // See https://webcompat.com/issues/15953
655 nsIFrame::ContentOffsets offsets = ptFrame->GetContentOffsetsFromPoint(
656 ptInFrame,
657 nsIFrame::SKIP_HIDDEN | nsIFrame::IGNORE_NATIVE_ANONYMOUS_SUBTREE);
658 if (offsets.content) {
659 RefPtr<nsFrameSelection> frameSelection = GetFrameSelection();
660 if (frameSelection) {
661 nsIFrame* theFrame = SelectionMovementUtils::GetFrameForNodeOffset(
662 offsets.content, offsets.offset, offsets.associate);
663 if (theFrame && theFrame != ptFrame) {
664 SetSelectionDragState(true);
665 frameSelection->HandleClick(
666 MOZ_KnownLive(offsets.content) /* bug 1636889 */,
667 offsets.StartOffset(), offsets.EndOffset(),
668 nsFrameSelection::FocusMode::kCollapseToNewPoint,
669 offsets.associate);
670 SetSelectionDragState(false);
671 ClearMaintainedSelection();
673 if (StaticPrefs::
674 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
675 mCarets.GetFirst()->SetAppearance(Appearance::Normal);
678 UpdateCarets();
679 ProvideHapticFeedback();
680 DispatchCaretStateChangedEvent(
681 CaretChangedReason::Longpressonemptycontent);
683 return NS_OK;
688 // Then try select a word under point.
689 nsresult rv = SelectWord(ptFrame, ptInFrame);
690 UpdateCarets();
691 ProvideHapticFeedback();
693 return rv;
696 void AccessibleCaretManager::OnScrollStart() {
697 AC_LOG("%s", __FUNCTION__);
699 nsAutoScriptBlocker scriptBlocker;
700 AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing);
701 mLayoutFlusher.mAllowFlushing = false;
703 Maybe<PresShell::AutoAssertNoFlush> assert;
704 if (mPresShell) {
705 assert.emplace(*mPresShell);
708 mIsScrollStarted = true;
710 if (mCarets.HasLogicallyVisibleCaret()) {
711 // Dispatch the event only if one of the carets is logically visible like in
712 // HideCaretsAndDispatchCaretStateChangedEvent().
713 DispatchCaretStateChangedEvent(CaretChangedReason::Scroll);
717 void AccessibleCaretManager::OnScrollEnd() {
718 nsAutoScriptBlocker scriptBlocker;
719 AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing);
720 mLayoutFlusher.mAllowFlushing = false;
722 Maybe<PresShell::AutoAssertNoFlush> assert;
723 if (mPresShell) {
724 assert.emplace(*mPresShell);
727 mIsScrollStarted = false;
729 if (GetCaretMode() == CaretMode::Cursor) {
730 if (!mCarets.GetFirst()->IsLogicallyVisible()) {
731 // If the caret is hidden (Appearance::None) due to blur, no
732 // need to update it.
733 return;
737 // For mouse and keyboard input, we don't want to show the carets.
738 if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
739 (mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_MOUSE ||
740 mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_KEYBOARD)) {
741 AC_LOG("%s: HideCaretsAndDispatchCaretStateChangedEvent()", __FUNCTION__);
742 HideCaretsAndDispatchCaretStateChangedEvent();
743 return;
746 AC_LOG("%s: UpdateCarets()", __FUNCTION__);
747 UpdateCarets();
750 void AccessibleCaretManager::OnScrollPositionChanged() {
751 nsAutoScriptBlocker scriptBlocker;
752 AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing);
753 mLayoutFlusher.mAllowFlushing = false;
755 Maybe<PresShell::AutoAssertNoFlush> assert;
756 if (mPresShell) {
757 assert.emplace(*mPresShell);
760 if (mCarets.HasLogicallyVisibleCaret()) {
761 if (mIsScrollStarted) {
762 // We don't want extra CaretStateChangedEvents dispatched when user is
763 // scrolling the page.
764 AC_LOG("%s: UpdateCarets(RespectOldAppearance | DispatchNoEvent)",
765 __FUNCTION__);
766 UpdateCarets({UpdateCaretsHint::RespectOldAppearance,
767 UpdateCaretsHint::DispatchNoEvent});
768 } else {
769 AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__);
770 UpdateCarets(UpdateCaretsHint::RespectOldAppearance);
775 void AccessibleCaretManager::OnReflow() {
776 nsAutoScriptBlocker scriptBlocker;
777 AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing);
778 mLayoutFlusher.mAllowFlushing = false;
780 Maybe<PresShell::AutoAssertNoFlush> assert;
781 if (mPresShell) {
782 assert.emplace(*mPresShell);
785 if (mCarets.HasLogicallyVisibleCaret()) {
786 AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__);
787 UpdateCarets(UpdateCaretsHint::RespectOldAppearance);
791 void AccessibleCaretManager::OnBlur() {
792 AC_LOG("%s: HideCaretsAndDispatchCaretStateChangedEvent()", __FUNCTION__);
793 HideCaretsAndDispatchCaretStateChangedEvent();
796 void AccessibleCaretManager::OnKeyboardEvent() {
797 if (GetCaretMode() == CaretMode::Cursor) {
798 AC_LOG("%s: HideCaretsAndDispatchCaretStateChangedEvent()", __FUNCTION__);
799 HideCaretsAndDispatchCaretStateChangedEvent();
803 void AccessibleCaretManager::SetLastInputSource(uint16_t aInputSource) {
804 mLastInputSource = aInputSource;
807 bool AccessibleCaretManager::ShouldDisableApz() const {
808 return mDesiredAsyncPanZoomState.Get() ==
809 DesiredAsyncPanZoomState::Value::Disabled;
812 Selection* AccessibleCaretManager::GetSelection() const {
813 RefPtr<nsFrameSelection> fs = GetFrameSelection();
814 if (!fs) {
815 return nullptr;
817 return fs->GetSelection(SelectionType::eNormal);
820 already_AddRefed<nsFrameSelection> AccessibleCaretManager::GetFrameSelection()
821 const {
822 if (!mPresShell) {
823 return nullptr;
826 // Prevent us from touching the nsFrameSelection associated with other
827 // PresShell.
828 RefPtr<nsFrameSelection> fs = mPresShell->GetLastFocusedFrameSelection();
829 if (!fs || fs->GetPresShell() != mPresShell) {
830 return nullptr;
833 return fs.forget();
836 nsAutoString AccessibleCaretManager::StringifiedSelection() const {
837 nsAutoString str;
838 RefPtr<Selection> selection = GetSelection();
839 if (selection) {
840 selection->Stringify(str, mLayoutFlusher.mAllowFlushing
841 ? Selection::FlushFrames::Yes
842 : Selection::FlushFrames::No);
844 return str;
847 // static
848 Element* AccessibleCaretManager::GetEditingHostForFrame(
849 const nsIFrame* aFrame) {
850 if (!aFrame) {
851 return nullptr;
854 auto content = aFrame->GetContent();
855 if (!content) {
856 return nullptr;
859 return content->GetEditingHost();
862 AccessibleCaretManager::CaretMode AccessibleCaretManager::GetCaretMode() const {
863 const Selection* selection = GetSelection();
864 if (!selection) {
865 return CaretMode::None;
868 const uint32_t rangeCount = selection->RangeCount();
869 if (rangeCount <= 0) {
870 return CaretMode::None;
873 const nsFocusManager* fm = nsFocusManager::GetFocusManager();
874 MOZ_ASSERT(fm);
875 if (fm->GetFocusedWindow() != mPresShell->GetDocument()->GetWindow()) {
876 // Hide carets if the window is not focused.
877 return CaretMode::None;
880 if (selection->IsCollapsed()) {
881 return CaretMode::Cursor;
884 return CaretMode::Selection;
887 nsIFrame* AccessibleCaretManager::GetFocusableFrame(nsIFrame* aFrame) const {
888 // This implementation is similar to EventStateManager::PostHandleEvent().
889 // Look for the nearest enclosing focusable frame.
890 nsIFrame* focusableFrame = aFrame;
891 while (focusableFrame) {
892 if (focusableFrame->IsFocusable(/* aWithMouse = */ true)) {
893 break;
895 focusableFrame = focusableFrame->GetParent();
897 return focusableFrame;
900 void AccessibleCaretManager::ChangeFocusToOrClearOldFocus(
901 nsIFrame* aFrame) const {
902 RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager();
903 MOZ_ASSERT(fm);
905 if (aFrame) {
906 nsIContent* focusableContent = aFrame->GetContent();
907 MOZ_ASSERT(focusableContent, "Focusable frame must have content!");
908 RefPtr<Element> focusableElement = Element::FromNode(focusableContent);
909 fm->SetFocus(focusableElement, nsIFocusManager::FLAG_BYLONGPRESS);
910 } else if (nsCOMPtr<nsPIDOMWindowOuter> win =
911 mPresShell->GetDocument()->GetWindow()) {
912 fm->ClearFocus(win);
913 fm->SetFocusedWindow(win);
917 nsresult AccessibleCaretManager::SelectWord(nsIFrame* aFrame,
918 const nsPoint& aPoint) const {
919 AC_LOGV("%s", __FUNCTION__);
921 SetSelectionDragState(true);
922 const RefPtr<nsPresContext> pinnedPresContext{mPresShell->GetPresContext()};
923 nsresult rs = aFrame->SelectByTypeAtPoint(pinnedPresContext, aPoint,
924 eSelectWord, eSelectWord, 0);
926 SetSelectionDragState(false);
927 ClearMaintainedSelection();
929 // Smart-select phone numbers if possible.
930 if (StaticPrefs::layout_accessiblecaret_extend_selection_for_phone_number()) {
931 SelectMoreIfPhoneNumber();
934 return rs;
937 void AccessibleCaretManager::SetSelectionDragState(bool aState) const {
938 RefPtr<nsFrameSelection> fs = GetFrameSelection();
939 if (fs) {
940 fs->SetDragState(aState);
944 bool AccessibleCaretManager::IsPhoneNumber(const nsAString& aCandidate) const {
945 RefPtr<Document> doc = mPresShell->GetDocument();
946 nsAutoString phoneNumberRegex(u"(^\\+)?[0-9 ,\\-.\\(\\)*#pw]{1,30}$"_ns);
947 return nsContentUtils::IsPatternMatching(aCandidate,
948 std::move(phoneNumberRegex), doc)
949 .valueOr(false);
952 void AccessibleCaretManager::SelectMoreIfPhoneNumber() const {
953 if (IsPhoneNumber(StringifiedSelection())) {
954 SetSelectionDirection(eDirNext);
955 ExtendPhoneNumberSelection(u"forward"_ns);
957 SetSelectionDirection(eDirPrevious);
958 ExtendPhoneNumberSelection(u"backward"_ns);
960 SetSelectionDirection(eDirNext);
964 void AccessibleCaretManager::ExtendPhoneNumberSelection(
965 const nsAString& aDirection) const {
966 if (!mPresShell) {
967 return;
970 // Extend the phone number selection until we find a boundary.
971 RefPtr<Selection> selection = GetSelection();
973 while (selection) {
974 const nsRange* anchorFocusRange = selection->GetAnchorFocusRange();
975 if (!anchorFocusRange) {
976 return;
979 // Backup the anchor focus range since both anchor node and focus node might
980 // be changed after calling Selection::Modify().
981 RefPtr<nsRange> oldAnchorFocusRange = anchorFocusRange->CloneRange();
983 // Save current focus node, focus offset and the selected text so that
984 // we can compare them with the modified ones later.
985 nsINode* oldFocusNode = selection->GetFocusNode();
986 uint32_t oldFocusOffset = selection->FocusOffset();
987 nsAutoString oldSelectedText = StringifiedSelection();
989 // Extend the selection by one char.
990 selection->Modify(u"extend"_ns, aDirection, u"character"_ns,
991 IgnoreErrors());
992 if (IsTerminated() == Terminated::Yes) {
993 return;
996 // If the selection didn't change, (can't extend further), we're done.
997 if (selection->GetFocusNode() == oldFocusNode &&
998 selection->FocusOffset() == oldFocusOffset) {
999 return;
1002 // If the changed selection isn't a valid phone number, we're done.
1003 // Also, if the selection was extended to a new block node, the string
1004 // returned by stringify() won't have a new line at the beginning or the
1005 // end of the string. Therefore, if either focus node or offset is
1006 // changed, but selected text is not changed, we're done, too.
1007 nsAutoString selectedText = StringifiedSelection();
1009 if (!IsPhoneNumber(selectedText) || oldSelectedText == selectedText) {
1010 // Backout the undesired selection extend, restore the old anchor focus
1011 // range before exit.
1012 selection->SetAnchorFocusToRange(oldAnchorFocusRange);
1013 return;
1018 void AccessibleCaretManager::SetSelectionDirection(nsDirection aDir) const {
1019 Selection* selection = GetSelection();
1020 if (selection) {
1021 selection->AdjustAnchorFocusForMultiRange(aDir);
1025 void AccessibleCaretManager::ClearMaintainedSelection() const {
1026 // Selection made by double-clicking for example will maintain the original
1027 // word selection. We should clear it so that we can drag caret freely.
1028 RefPtr<nsFrameSelection> fs = GetFrameSelection();
1029 if (fs) {
1030 fs->MaintainSelection(eSelectNoAmount);
1034 void AccessibleCaretManager::LayoutFlusher::MaybeFlush(
1035 const PresShell& aPresShell) {
1036 if (mAllowFlushing) {
1037 AutoRestore<bool> flushing(mFlushing);
1038 mFlushing = true;
1040 if (Document* doc = aPresShell.GetDocument()) {
1041 doc->FlushPendingNotifications(FlushType::Layout);
1042 // Don't access the PresShell after flushing, it could've become invalid.
1047 nsIFrame* AccessibleCaretManager::GetFrameForFirstRangeStartOrLastRangeEnd(
1048 nsDirection aDirection, int32_t* aOutOffset, nsIContent** aOutContent,
1049 int32_t* aOutContentOffset) const {
1050 if (!mPresShell) {
1051 return nullptr;
1054 MOZ_ASSERT(GetCaretMode() == CaretMode::Selection);
1055 MOZ_ASSERT(aOutOffset, "aOutOffset shouldn't be nullptr!");
1057 const nsRange* range = nullptr;
1058 RefPtr<nsINode> startNode;
1059 RefPtr<nsINode> endNode;
1060 int32_t nodeOffset = 0;
1061 CaretAssociationHint hint;
1063 RefPtr<Selection> selection = GetSelection();
1064 bool findInFirstRangeStart = aDirection == eDirNext;
1066 if (findInFirstRangeStart) {
1067 range = selection->GetRangeAt(0);
1068 startNode = range->GetStartContainer();
1069 endNode = range->GetEndContainer();
1070 nodeOffset = range->StartOffset();
1071 hint = CaretAssociationHint::After;
1072 } else {
1073 MOZ_ASSERT(selection->RangeCount() > 0);
1074 range = selection->GetRangeAt(selection->RangeCount() - 1);
1075 startNode = range->GetEndContainer();
1076 endNode = range->GetStartContainer();
1077 nodeOffset = range->EndOffset();
1078 hint = CaretAssociationHint::Before;
1081 nsCOMPtr<nsIContent> startContent = nsIContent::FromNodeOrNull(startNode);
1082 uint32_t outOffset = 0;
1083 nsIFrame* startFrame = SelectionMovementUtils::GetFrameForNodeOffset(
1084 startContent, nodeOffset, hint, &outOffset);
1085 *aOutOffset = static_cast<int32_t>(outOffset);
1087 if (!startFrame) {
1088 ErrorResult err;
1089 RefPtr<TreeWalker> walker = mPresShell->GetDocument()->CreateTreeWalker(
1090 *startNode, dom::NodeFilter_Binding::SHOW_ALL, nullptr, err);
1092 if (!walker) {
1093 return nullptr;
1096 startFrame = startContent ? startContent->GetPrimaryFrame() : nullptr;
1097 while (!startFrame && startNode != endNode) {
1098 startNode = findInFirstRangeStart ? walker->NextNode(err)
1099 : walker->PreviousNode(err);
1101 if (!startNode) {
1102 break;
1105 startContent = startNode->AsContent();
1106 startFrame = startContent ? startContent->GetPrimaryFrame() : nullptr;
1109 // We are walking among the nodes in the content tree, so the node offset
1110 // relative to startNode should be set to 0.
1111 nodeOffset = 0;
1112 *aOutOffset = 0;
1115 if (startFrame) {
1116 if (aOutContent) {
1117 startContent.forget(aOutContent);
1119 if (aOutContentOffset) {
1120 *aOutContentOffset = nodeOffset;
1124 return startFrame;
1127 bool AccessibleCaretManager::RestrictCaretDraggingOffsets(
1128 nsIFrame::ContentOffsets& aOffsets) {
1129 if (!mPresShell) {
1130 return false;
1133 MOZ_ASSERT(GetCaretMode() == CaretMode::Selection);
1135 nsDirection dir =
1136 mActiveCaret == mCarets.GetFirst() ? eDirPrevious : eDirNext;
1137 int32_t offset = 0;
1138 nsCOMPtr<nsIContent> content;
1139 int32_t contentOffset = 0;
1140 nsIFrame* frame = GetFrameForFirstRangeStartOrLastRangeEnd(
1141 dir, &offset, getter_AddRefs(content), &contentOffset);
1143 if (!frame) {
1144 return false;
1147 // Compare the active caret's new position (aOffsets) to the inactive caret's
1148 // position.
1149 NS_ASSERTION(contentOffset >= 0, "contentOffset should not be negative");
1150 const Maybe<int32_t> cmpToInactiveCaretPos =
1151 nsContentUtils::ComparePoints_AllowNegativeOffsets(
1152 aOffsets.content, aOffsets.StartOffset(), content, contentOffset);
1153 if (NS_WARN_IF(!cmpToInactiveCaretPos)) {
1154 // Potentially handle this properly when Selection across Shadow DOM
1155 // boundary is implemented
1156 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1157 return false;
1160 // Move one character (in the direction of dir) from the inactive caret's
1161 // position. This is the limit for the active caret's new position.
1162 PeekOffsetStruct limit(
1163 eSelectCluster, dir, offset, nsPoint(0, 0),
1164 {PeekOffsetOption::JumpLines, PeekOffsetOption::StopAtScroller});
1165 nsresult rv = frame->PeekOffset(&limit);
1166 if (NS_FAILED(rv)) {
1167 limit.mResultContent = content;
1168 limit.mContentOffset = contentOffset;
1171 // Compare the active caret's new position (aOffsets) to the limit.
1172 NS_ASSERTION(limit.mContentOffset >= 0,
1173 "limit.mContentOffset should not be negative");
1174 const Maybe<int32_t> cmpToLimit =
1175 nsContentUtils::ComparePoints_AllowNegativeOffsets(
1176 aOffsets.content, aOffsets.StartOffset(), limit.mResultContent,
1177 limit.mContentOffset);
1178 if (NS_WARN_IF(!cmpToLimit)) {
1179 // Potentially handle this properly when Selection across Shadow DOM
1180 // boundary is implemented
1181 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1182 return false;
1185 auto SetOffsetsToLimit = [&aOffsets, &limit]() {
1186 aOffsets.content = limit.mResultContent;
1187 aOffsets.offset = limit.mContentOffset;
1188 aOffsets.secondaryOffset = limit.mContentOffset;
1191 if (!StaticPrefs::
1192 layout_accessiblecaret_allow_dragging_across_other_caret()) {
1193 if ((mActiveCaret == mCarets.GetFirst() && *cmpToLimit == 1) ||
1194 (mActiveCaret == mCarets.GetSecond() && *cmpToLimit == -1)) {
1195 // The active caret's position is past the limit, which we don't allow
1196 // here. So set it to the limit, resulting in one character being
1197 // selected.
1198 SetOffsetsToLimit();
1200 } else {
1201 switch (*cmpToInactiveCaretPos) {
1202 case 0:
1203 // The active caret's position is the same as the position of the
1204 // inactive caret. So set it to the limit to prevent the selection from
1205 // being collapsed, resulting in one character being selected.
1206 SetOffsetsToLimit();
1207 break;
1208 case 1:
1209 if (mActiveCaret == mCarets.GetFirst()) {
1210 // First caret was moved across the second caret. After making change
1211 // to the selection, the user will drag the second caret.
1212 mActiveCaret = mCarets.GetSecond();
1214 break;
1215 case -1:
1216 if (mActiveCaret == mCarets.GetSecond()) {
1217 // Second caret was moved across the first caret. After making change
1218 // to the selection, the user will drag the first caret.
1219 mActiveCaret = mCarets.GetFirst();
1221 break;
1225 return true;
1228 bool AccessibleCaretManager::CompareTreePosition(nsIFrame* aStartFrame,
1229 nsIFrame* aEndFrame) const {
1230 return (aStartFrame && aEndFrame &&
1231 nsLayoutUtils::CompareTreePosition(aStartFrame, aEndFrame) <= 0);
1234 nsresult AccessibleCaretManager::DragCaretInternal(const nsPoint& aPoint) {
1235 MOZ_ASSERT(mPresShell);
1237 nsIFrame* rootFrame = mPresShell->GetRootFrame();
1238 MOZ_ASSERT(rootFrame, "We need root frame to compute caret dragging!");
1240 nsPoint point = AdjustDragBoundary(
1241 nsPoint(aPoint.x, aPoint.y + mOffsetYToCaretLogicalPosition));
1243 // Find out which content we point to
1245 nsIFrame* ptFrame = nsLayoutUtils::GetFrameForPoint(
1246 RelativeTo{rootFrame}, point, GetHitTestOptions());
1247 if (!ptFrame) {
1248 return NS_ERROR_FAILURE;
1251 RefPtr<nsFrameSelection> fs = GetFrameSelection();
1252 MOZ_ASSERT(fs);
1254 nsresult result;
1255 nsIFrame* newFrame = nullptr;
1256 nsPoint newPoint;
1257 nsPoint ptInFrame = point;
1258 nsLayoutUtils::TransformPoint(RelativeTo{rootFrame}, RelativeTo{ptFrame},
1259 ptInFrame);
1260 result = fs->ConstrainFrameAndPointToAnchorSubtree(ptFrame, ptInFrame,
1261 &newFrame, newPoint);
1262 if (NS_FAILED(result) || !newFrame) {
1263 return NS_ERROR_FAILURE;
1266 if (!newFrame->IsSelectable(nullptr)) {
1267 return NS_ERROR_FAILURE;
1270 nsIFrame::ContentOffsets offsets = newFrame->GetContentOffsetsFromPoint(
1271 newPoint, nsIFrame::IGNORE_NATIVE_ANONYMOUS_SUBTREE);
1272 if (offsets.IsNull()) {
1273 return NS_ERROR_FAILURE;
1276 if (GetCaretMode() == CaretMode::Selection &&
1277 !RestrictCaretDraggingOffsets(offsets)) {
1278 return NS_ERROR_FAILURE;
1281 ClearMaintainedSelection();
1283 const nsFrameSelection::FocusMode focusMode =
1284 (GetCaretMode() == CaretMode::Selection)
1285 ? nsFrameSelection::FocusMode::kExtendSelection
1286 : nsFrameSelection::FocusMode::kCollapseToNewPoint;
1287 fs->HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */,
1288 offsets.StartOffset(), offsets.EndOffset(), focusMode,
1289 offsets.associate);
1290 return NS_OK;
1293 // static
1294 nsRect AccessibleCaretManager::GetAllChildFrameRectsUnion(nsIFrame* aFrame) {
1295 nsRect unionRect;
1297 // Drill through scroll frames, we don't want to include scrollbar child
1298 // frames below.
1299 for (nsIFrame* frame = aFrame->GetContentInsertionFrame(); frame;
1300 frame = frame->GetNextContinuation()) {
1301 nsRect frameRect;
1303 for (const auto& childList : frame->ChildLists()) {
1304 // Loop all children to union their scrollable overflow rect.
1305 for (nsIFrame* child : childList.mList) {
1306 nsRect childRect = child->ScrollableOverflowRectRelativeToSelf();
1307 nsLayoutUtils::TransformRect(child, frame, childRect);
1309 // A TextFrame containing only '\n' has positive height and width 0, or
1310 // positive width and height 0 if it's vertical. Need to use UnionEdges
1311 // to add its rect. BRFrame rect should be non-empty.
1312 if (childRect.IsEmpty()) {
1313 frameRect = frameRect.UnionEdges(childRect);
1314 } else {
1315 frameRect = frameRect.Union(childRect);
1320 MOZ_ASSERT(!frameRect.IsEmpty(),
1321 "Editable frames should have at least one BRFrame child to make "
1322 "frameRect non-empty!");
1323 if (frame != aFrame) {
1324 nsLayoutUtils::TransformRect(frame, aFrame, frameRect);
1326 unionRect = unionRect.Union(frameRect);
1329 return unionRect;
1332 nsPoint AccessibleCaretManager::AdjustDragBoundary(
1333 const nsPoint& aPoint) const {
1334 nsPoint adjustedPoint = aPoint;
1336 int32_t focusOffset = 0;
1337 nsIFrame* focusFrame =
1338 nsCaret::GetFrameAndOffset(GetSelection(), nullptr, 0, &focusOffset);
1339 Element* editingHost = GetEditingHostForFrame(focusFrame);
1341 if (editingHost) {
1342 nsIFrame* editingHostFrame = editingHost->GetPrimaryFrame();
1343 if (editingHostFrame) {
1344 nsRect boundary =
1345 AccessibleCaretManager::GetAllChildFrameRectsUnion(editingHostFrame);
1346 nsLayoutUtils::TransformRect(editingHostFrame, mPresShell->GetRootFrame(),
1347 boundary);
1349 // Shrink the rect to make sure we never hit the boundary.
1350 boundary.Deflate(kBoundaryAppUnits);
1352 adjustedPoint = boundary.ClampPoint(adjustedPoint);
1356 if (GetCaretMode() == CaretMode::Selection &&
1357 !StaticPrefs::
1358 layout_accessiblecaret_allow_dragging_across_other_caret()) {
1359 // Bug 1068474: Adjust the Y-coordinate so that the carets won't be in tilt
1360 // mode when a caret is being dragged surpass the other caret.
1362 // For example, when dragging the second caret, the horizontal boundary
1363 // (lower bound) of its Y-coordinate is the logical position of the first
1364 // caret. Likewise, when dragging the first caret, the horizontal boundary
1365 // (upper bound) of its Y-coordinate is the logical position of the second
1366 // caret.
1367 if (mActiveCaret == mCarets.GetFirst()) {
1368 nscoord dragDownBoundaryY = mCarets.GetSecond()->LogicalPosition().y;
1369 if (dragDownBoundaryY > 0 && adjustedPoint.y > dragDownBoundaryY) {
1370 adjustedPoint.y = dragDownBoundaryY;
1372 } else {
1373 nscoord dragUpBoundaryY = mCarets.GetFirst()->LogicalPosition().y;
1374 if (adjustedPoint.y < dragUpBoundaryY) {
1375 adjustedPoint.y = dragUpBoundaryY;
1380 return adjustedPoint;
1383 void AccessibleCaretManager::StartSelectionAutoScrollTimer(
1384 const nsPoint& aPoint) const {
1385 Selection* selection = GetSelection();
1386 MOZ_ASSERT(selection);
1388 nsIFrame* anchorFrame = selection->GetPrimaryFrameForAnchorNode();
1389 if (!anchorFrame) {
1390 return;
1393 nsIScrollableFrame* scrollFrame = nsLayoutUtils::GetNearestScrollableFrame(
1394 anchorFrame, nsLayoutUtils::SCROLLABLE_SAME_DOC |
1395 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
1396 if (!scrollFrame) {
1397 return;
1400 nsIFrame* capturingFrame = scrollFrame->GetScrolledFrame();
1401 if (!capturingFrame) {
1402 return;
1405 nsIFrame* rootFrame = mPresShell->GetRootFrame();
1406 MOZ_ASSERT(rootFrame);
1407 nsPoint ptInScrolled = aPoint;
1408 nsLayoutUtils::TransformPoint(RelativeTo{rootFrame},
1409 RelativeTo{capturingFrame}, ptInScrolled);
1411 RefPtr<nsFrameSelection> fs = GetFrameSelection();
1412 MOZ_ASSERT(fs);
1413 fs->StartAutoScrollTimer(capturingFrame, ptInScrolled, kAutoScrollTimerDelay);
1416 void AccessibleCaretManager::StopSelectionAutoScrollTimer() const {
1417 RefPtr<nsFrameSelection> fs = GetFrameSelection();
1418 MOZ_ASSERT(fs);
1419 fs->StopAutoScrollTimer();
1422 void AccessibleCaretManager::DispatchCaretStateChangedEvent(
1423 CaretChangedReason aReason, const nsPoint* aPoint) {
1424 if (MaybeFlushLayout() == Terminated::Yes) {
1425 return;
1428 const Selection* sel = GetSelection();
1429 if (!sel) {
1430 return;
1433 Document* doc = mPresShell->GetDocument();
1434 MOZ_ASSERT(doc);
1436 CaretStateChangedEventInit init;
1437 init.mBubbles = true;
1439 const nsRange* range = sel->GetAnchorFocusRange();
1440 nsINode* commonAncestorNode = nullptr;
1441 if (range) {
1442 commonAncestorNode = range->GetClosestCommonInclusiveAncestor();
1445 if (!commonAncestorNode) {
1446 commonAncestorNode = sel->GetFrameSelection()->GetAncestorLimiter();
1449 RefPtr<DOMRect> domRect = new DOMRect(ToSupports(doc));
1450 nsRect rect = nsLayoutUtils::GetSelectionBoundingRect(sel);
1452 nsIFrame* commonAncestorFrame = nullptr;
1453 nsIFrame* rootFrame = mPresShell->GetRootFrame();
1455 if (commonAncestorNode && commonAncestorNode->IsContent()) {
1456 commonAncestorFrame = commonAncestorNode->AsContent()->GetPrimaryFrame();
1459 if (commonAncestorFrame && rootFrame) {
1460 nsLayoutUtils::TransformRect(rootFrame, commonAncestorFrame, rect);
1461 nsRect clampedRect =
1462 nsLayoutUtils::ClampRectToScrollFrames(commonAncestorFrame, rect);
1463 nsLayoutUtils::TransformRect(commonAncestorFrame, rootFrame, clampedRect);
1464 rect = clampedRect;
1465 init.mSelectionVisible = !clampedRect.IsEmpty();
1466 } else {
1467 init.mSelectionVisible = true;
1470 domRect->SetLayoutRect(rect);
1472 // Send isEditable info w/ event detail. This info can help determine
1473 // whether to show cut command on selection dialog or not.
1474 init.mSelectionEditable =
1475 commonAncestorFrame && GetEditingHostForFrame(commonAncestorFrame);
1477 init.mBoundingClientRect = domRect;
1478 init.mReason = aReason;
1479 init.mCollapsed = sel->IsCollapsed();
1480 init.mCaretVisible = mCarets.HasLogicallyVisibleCaret();
1481 init.mCaretVisuallyVisible = mCarets.HasVisuallyVisibleCaret();
1482 init.mSelectedTextContent = StringifiedSelection();
1484 if (aPoint) {
1485 CSSIntPoint pt = CSSPixel::FromAppUnitsRounded(*aPoint);
1486 init.mClientX = pt.x;
1487 init.mClientY = pt.y;
1490 RefPtr<CaretStateChangedEvent> event = CaretStateChangedEvent::Constructor(
1491 doc, u"mozcaretstatechanged"_ns, init);
1492 event->SetTrusted(true);
1494 AC_LOG("%s: reason %" PRIu32 ", collapsed %d, caretVisible %" PRIu32,
1495 __FUNCTION__, static_cast<uint32_t>(init.mReason), init.mCollapsed,
1496 static_cast<uint32_t>(init.mCaretVisible));
1498 (new AsyncEventDispatcher(doc, event.forget(), ChromeOnlyDispatch::eYes))
1499 ->PostDOMEvent();
1502 AccessibleCaretManager::Carets::Carets(UniquePtr<AccessibleCaret> aFirst,
1503 UniquePtr<AccessibleCaret> aSecond)
1504 : mFirst{std::move(aFirst)}, mSecond{std::move(aSecond)} {}
1506 } // namespace mozilla