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"
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/dom/Element.h"
17 #include "mozilla/dom/MouseEventBinding.h"
18 #include "mozilla/dom/NodeFilterBinding.h"
19 #include "mozilla/dom/Selection.h"
20 #include "mozilla/dom/TreeWalker.h"
21 #include "mozilla/IMEStateManager.h"
22 #include "mozilla/IntegerPrintfMacros.h"
23 #include "mozilla/PresShell.h"
24 #include "mozilla/StaticAnalysisFunctions.h"
25 #include "mozilla/StaticPrefs_layout.h"
27 #include "nsContainerFrame.h"
28 #include "nsContentUtils.h"
30 #include "nsFocusManager.h"
32 #include "nsFrameSelection.h"
33 #include "nsGenericHTMLElement.h"
34 #include "nsIHapticFeedback.h"
35 #include "nsIScrollableFrame.h"
36 #include "nsLayoutUtils.h"
37 #include "nsServiceManagerUtils.h"
42 #define AC_LOG(message, ...) \
43 AC_LOG_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
46 #define AC_LOGV(message, ...) \
47 AC_LOGV_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
50 using Appearance
= AccessibleCaret::Appearance
;
51 using PositionChangedResult
= AccessibleCaret::PositionChangedResult
;
53 #define AC_PROCESS_ENUM_TO_STREAM(e) \
57 std::ostream
& operator<<(std::ostream
& aStream
,
58 const AccessibleCaretManager::CaretMode
& aCaretMode
) {
59 using CaretMode
= AccessibleCaretManager::CaretMode
;
61 AC_PROCESS_ENUM_TO_STREAM(CaretMode::None
);
62 AC_PROCESS_ENUM_TO_STREAM(CaretMode::Cursor
);
63 AC_PROCESS_ENUM_TO_STREAM(CaretMode::Selection
);
68 std::ostream
& operator<<(
69 std::ostream
& aStream
,
70 const AccessibleCaretManager::UpdateCaretsHint
& aHint
) {
71 using UpdateCaretsHint
= AccessibleCaretManager::UpdateCaretsHint
;
73 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::Default
);
74 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::RespectOldAppearance
);
75 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::DispatchNoEvent
);
79 #undef AC_PROCESS_ENUM_TO_STREAM
81 AccessibleCaretManager::AccessibleCaretManager(PresShell
* aPresShell
)
82 : AccessibleCaretManager
{
84 Carets
{aPresShell
? MakeUnique
<AccessibleCaret
>(aPresShell
) : nullptr,
85 aPresShell
? MakeUnique
<AccessibleCaret
>(aPresShell
)
88 AccessibleCaretManager::AccessibleCaretManager(PresShell
* aPresShell
,
90 : mPresShell
{aPresShell
}, mCarets
{std::move(aCarets
)} {}
92 AccessibleCaretManager::LayoutFlusher::~LayoutFlusher() {
93 MOZ_RELEASE_ASSERT(!mFlushing
, "Going away in MaybeFlush? Bad!");
96 void AccessibleCaretManager::Terminate() {
98 mActiveCaret
= nullptr;
102 nsresult
AccessibleCaretManager::OnSelectionChanged(Document
* aDoc
,
105 Selection
* selection
= GetSelection();
106 AC_LOG("%s: aSel: %p, GetSelection(): %p, aReason: %d", __FUNCTION__
, aSel
,
108 if (aSel
!= selection
) {
112 // eSetSelection events from the Fennec widget IME can be generated
113 // by autoSuggest / autoCorrect composition changes, or by TYPE_REPLACE_TEXT
114 // actions, either positioning cursor for text insert, or selecting
115 // text-to-be-replaced. None should affect AccessibleCaret visibility.
116 if (aReason
& nsISelectionListener::IME_REASON
) {
120 // Move the cursor by JavaScript or unknown internal call.
121 if (aReason
== nsISelectionListener::NO_REASON
||
122 aReason
== nsISelectionListener::JS_REASON
) {
123 auto mode
= static_cast<ScriptUpdateMode
>(
124 StaticPrefs::layout_accessiblecaret_script_change_update_mode());
125 if (mode
== kScriptAlwaysShow
||
126 (mode
== kScriptUpdateVisible
&& mCarets
.HasLogicallyVisibleCaret())) {
130 // Default for NO_REASON is to make hidden.
131 HideCaretsAndDispatchCaretStateChangedEvent();
135 // Move cursor by keyboard.
136 if (aReason
& nsISelectionListener::KEYPRESS_REASON
) {
137 HideCaretsAndDispatchCaretStateChangedEvent();
141 // OnBlur() might be called between mouse down and mouse up, so we hide carets
142 // upon mouse down anyway, and update carets upon mouse up.
143 if (aReason
& nsISelectionListener::MOUSEDOWN_REASON
) {
144 HideCaretsAndDispatchCaretStateChangedEvent();
148 // Range will collapse after cutting or copying text.
149 if (aReason
& (nsISelectionListener::COLLAPSETOSTART_REASON
|
150 nsISelectionListener::COLLAPSETOEND_REASON
)) {
151 HideCaretsAndDispatchCaretStateChangedEvent();
155 // For mouse input we don't want to show the carets.
156 if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
157 mLastInputSource
== MouseEvent_Binding::MOZ_SOURCE_MOUSE
) {
158 HideCaretsAndDispatchCaretStateChangedEvent();
162 // When we want to hide the carets for mouse input, hide them for select
163 // all action fired by keyboard as well.
164 if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
165 mLastInputSource
== MouseEvent_Binding::MOZ_SOURCE_KEYBOARD
&&
166 (aReason
& nsISelectionListener::SELECTALL_REASON
)) {
167 HideCaretsAndDispatchCaretStateChangedEvent();
175 void AccessibleCaretManager::HideCaretsAndDispatchCaretStateChangedEvent() {
176 if (mCarets
.HasLogicallyVisibleCaret()) {
177 AC_LOG("%s", __FUNCTION__
);
178 mCarets
.GetFirst()->SetAppearance(Appearance::None
);
179 mCarets
.GetSecond()->SetAppearance(Appearance::None
);
180 mIsCaretPositionChanged
= false;
181 DispatchCaretStateChangedEvent(CaretChangedReason::Visibilitychange
);
185 auto AccessibleCaretManager::MaybeFlushLayout() -> Terminated
{
187 // `MaybeFlush` doesn't access the PresShell after flushing, so it's OK to
189 mLayoutFlusher
.MaybeFlush(MOZ_KnownLive(*mPresShell
));
192 return IsTerminated();
195 void AccessibleCaretManager::UpdateCarets(const UpdateCaretsHintSet
& aHint
) {
196 if (MaybeFlushLayout() == Terminated::Yes
) {
200 mLastUpdateCaretMode
= GetCaretMode();
202 switch (mLastUpdateCaretMode
) {
203 case CaretMode::None
:
204 HideCaretsAndDispatchCaretStateChangedEvent();
206 case CaretMode::Cursor
:
207 UpdateCaretsForCursorMode(aHint
);
209 case CaretMode::Selection
:
210 UpdateCaretsForSelectionMode(aHint
);
214 mDesiredAsyncPanZoomState
.Update(*this);
217 bool AccessibleCaretManager::IsCaretDisplayableInCursorMode(
218 nsIFrame
** aOutFrame
, int32_t* aOutOffset
) const {
219 RefPtr
<nsCaret
> caret
= mPresShell
->GetCaret();
220 if (!caret
|| !caret
->IsVisible()) {
226 nsCaret::GetFrameAndOffset(GetSelection(), nullptr, 0, &offset
);
232 if (!GetEditingHostForFrame(frame
)) {
241 *aOutOffset
= offset
;
247 bool AccessibleCaretManager::HasNonEmptyTextContent(nsINode
* aNode
) const {
248 return nsContentUtils::HasNonEmptyTextContent(
249 aNode
, nsContentUtils::eRecurseIntoChildren
);
252 void AccessibleCaretManager::UpdateCaretsForCursorMode(
253 const UpdateCaretsHintSet
& aHints
) {
254 AC_LOG("%s, selection: %p", __FUNCTION__
, GetSelection());
257 nsIFrame
* frame
= nullptr;
258 if (!IsCaretDisplayableInCursorMode(&frame
, &offset
)) {
259 HideCaretsAndDispatchCaretStateChangedEvent();
263 PositionChangedResult result
= mCarets
.GetFirst()->SetPosition(frame
, offset
);
266 case PositionChangedResult::NotChanged
:
267 case PositionChangedResult::Position
:
268 case PositionChangedResult::Zoom
:
269 if (!aHints
.contains(UpdateCaretsHint::RespectOldAppearance
)) {
270 if (HasNonEmptyTextContent(GetEditingHostForFrame(frame
))) {
271 mCarets
.GetFirst()->SetAppearance(Appearance::Normal
);
274 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
275 if (mCarets
.GetFirst()->IsLogicallyVisible()) {
276 // Possible cases are: 1) SelectWordOrShortcut() sets the
277 // appearance to Normal. 2) When the caret is out of viewport and
278 // now scrolling into viewport, it has appearance NormalNotShown.
279 mCarets
.GetFirst()->SetAppearance(Appearance::Normal
);
281 // Possible cases are: a) Single tap on current empty content;
282 // OnSelectionChanged() sets the appearance to None due to
283 // MOUSEDOWN_REASON. b) Single tap on other empty content;
284 // OnBlur() sets the appearance to None.
286 // Do nothing to make the appearance remains None so that it can
287 // be distinguished from case 2). Also do not set the appearance
288 // to NormalNotShown here like the default update behavior.
291 mCarets
.GetFirst()->SetAppearance(Appearance::NormalNotShown
);
296 case PositionChangedResult::Invisible
:
297 mCarets
.GetFirst()->SetAppearance(Appearance::NormalNotShown
);
301 mCarets
.GetSecond()->SetAppearance(Appearance::None
);
303 mIsCaretPositionChanged
= (result
== PositionChangedResult::Position
);
305 if (!aHints
.contains(UpdateCaretsHint::DispatchNoEvent
) && !mActiveCaret
) {
306 DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition
);
310 void AccessibleCaretManager::UpdateCaretsForSelectionMode(
311 const UpdateCaretsHintSet
& aHints
) {
312 AC_LOG("%s: selection: %p", __FUNCTION__
, GetSelection());
314 int32_t startOffset
= 0;
315 nsIFrame
* startFrame
=
316 GetFrameForFirstRangeStartOrLastRangeEnd(eDirNext
, &startOffset
);
318 int32_t endOffset
= 0;
320 GetFrameForFirstRangeStartOrLastRangeEnd(eDirPrevious
, &endOffset
);
322 if (!CompareTreePosition(startFrame
, endFrame
)) {
323 // XXX: Do we really have to hide carets if this condition isn't satisfied?
324 HideCaretsAndDispatchCaretStateChangedEvent();
328 auto updateSingleCaret
= [aHints
](AccessibleCaret
* aCaret
, nsIFrame
* aFrame
,
329 int32_t aOffset
) -> PositionChangedResult
{
330 PositionChangedResult result
= aCaret
->SetPosition(aFrame
, aOffset
);
333 case PositionChangedResult::NotChanged
:
334 case PositionChangedResult::Position
:
335 case PositionChangedResult::Zoom
:
336 if (!aHints
.contains(UpdateCaretsHint::RespectOldAppearance
)) {
337 aCaret
->SetAppearance(Appearance::Normal
);
341 case PositionChangedResult::Invisible
:
342 aCaret
->SetAppearance(Appearance::NormalNotShown
);
348 PositionChangedResult firstCaretResult
=
349 updateSingleCaret(mCarets
.GetFirst(), startFrame
, startOffset
);
350 PositionChangedResult secondCaretResult
=
351 updateSingleCaret(mCarets
.GetSecond(), endFrame
, endOffset
);
353 mIsCaretPositionChanged
=
354 firstCaretResult
== PositionChangedResult::Position
||
355 secondCaretResult
== PositionChangedResult::Position
;
357 if (mIsCaretPositionChanged
) {
358 // Flush layout to make the carets intersection correct.
359 if (MaybeFlushLayout() == Terminated::Yes
) {
364 if (!aHints
.contains(UpdateCaretsHint::RespectOldAppearance
)) {
365 // Only check for tilt carets when the caller doesn't ask us to preserve
366 // old appearance. Otherwise we might override the appearance set by the
368 if (StaticPrefs::layout_accessiblecaret_always_tilt()) {
369 UpdateCaretsForAlwaysTilt(startFrame
, endFrame
);
371 UpdateCaretsForOverlappingTilt();
375 if (!aHints
.contains(UpdateCaretsHint::DispatchNoEvent
) && !mActiveCaret
) {
376 DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition
);
380 void AccessibleCaretManager::DesiredAsyncPanZoomState::Update(
381 const AccessibleCaretManager
& aAccessibleCaretManager
) {
382 if (aAccessibleCaretManager
.mActiveCaret
) {
383 // No need to disable APZ when dragging the caret.
384 mValue
= Value::Enabled
;
388 if (aAccessibleCaretManager
.mIsScrollStarted
) {
389 // During scrolling, the caret's position is changed only if it is in a
390 // position:fixed or a "stuck" position:sticky frame subtree.
391 mValue
= aAccessibleCaretManager
.mIsCaretPositionChanged
? Value::Disabled
396 // For other cases, we can only reliably detect whether the caret is in a
397 // position:fixed frame subtree.
398 switch (aAccessibleCaretManager
.mLastUpdateCaretMode
) {
399 case CaretMode::None
:
400 mValue
= Value::Enabled
;
402 case CaretMode::Cursor
:
404 (aAccessibleCaretManager
.mCarets
.GetFirst()->IsVisuallyVisible() &&
405 aAccessibleCaretManager
.mCarets
.GetFirst()
406 ->IsInPositionFixedSubtree())
410 case CaretMode::Selection
:
412 ((aAccessibleCaretManager
.mCarets
.GetFirst()->IsVisuallyVisible() &&
413 aAccessibleCaretManager
.mCarets
.GetFirst()
414 ->IsInPositionFixedSubtree()) ||
415 (aAccessibleCaretManager
.mCarets
.GetSecond()->IsVisuallyVisible() &&
416 aAccessibleCaretManager
.mCarets
.GetSecond()
417 ->IsInPositionFixedSubtree()))
424 bool AccessibleCaretManager::UpdateCaretsForOverlappingTilt() {
425 if (!mCarets
.GetFirst()->IsVisuallyVisible() ||
426 !mCarets
.GetSecond()->IsVisuallyVisible()) {
430 if (!mCarets
.GetFirst()->Intersects(*mCarets
.GetSecond())) {
431 mCarets
.GetFirst()->SetAppearance(Appearance::Normal
);
432 mCarets
.GetSecond()->SetAppearance(Appearance::Normal
);
436 if (mCarets
.GetFirst()->LogicalPosition().x
<=
437 mCarets
.GetSecond()->LogicalPosition().x
) {
438 mCarets
.GetFirst()->SetAppearance(Appearance::Left
);
439 mCarets
.GetSecond()->SetAppearance(Appearance::Right
);
441 mCarets
.GetFirst()->SetAppearance(Appearance::Right
);
442 mCarets
.GetSecond()->SetAppearance(Appearance::Left
);
448 void AccessibleCaretManager::UpdateCaretsForAlwaysTilt(
449 const nsIFrame
* aStartFrame
, const nsIFrame
* aEndFrame
) {
450 // When a short LTR word in RTL environment is selected, the two carets
451 // tilted inward might be overlapped. Make them tilt outward.
452 if (UpdateCaretsForOverlappingTilt()) {
456 if (mCarets
.GetFirst()->IsVisuallyVisible()) {
457 auto startFrameWritingMode
= aStartFrame
->GetWritingMode();
458 mCarets
.GetFirst()->SetAppearance(startFrameWritingMode
.IsBidiLTR()
460 : Appearance::Right
);
462 if (mCarets
.GetSecond()->IsVisuallyVisible()) {
463 auto endFrameWritingMode
= aEndFrame
->GetWritingMode();
464 mCarets
.GetSecond()->SetAppearance(
465 endFrameWritingMode
.IsBidiLTR() ? Appearance::Right
: Appearance::Left
);
469 void AccessibleCaretManager::ProvideHapticFeedback() {
470 if (StaticPrefs::layout_accessiblecaret_hapticfeedback()) {
471 if (nsCOMPtr
<nsIHapticFeedback
> haptic
=
472 do_GetService("@mozilla.org/widget/hapticfeedback;1")) {
473 haptic
->PerformSimpleAction(haptic
->LongPress
);
478 nsresult
AccessibleCaretManager::PressCaret(const nsPoint
& aPoint
,
479 EventClassID aEventClass
) {
480 nsresult rv
= NS_ERROR_FAILURE
;
482 MOZ_ASSERT(aEventClass
== eMouseEventClass
|| aEventClass
== eTouchEventClass
,
483 "Unexpected event class!");
485 using TouchArea
= AccessibleCaret::TouchArea
;
486 TouchArea touchArea
=
487 aEventClass
== eMouseEventClass
? TouchArea::CaretImage
: TouchArea::Full
;
489 if (mCarets
.GetFirst()->Contains(aPoint
, touchArea
)) {
490 mActiveCaret
= mCarets
.GetFirst();
491 SetSelectionDirection(eDirPrevious
);
492 } else if (mCarets
.GetSecond()->Contains(aPoint
, touchArea
)) {
493 mActiveCaret
= mCarets
.GetSecond();
494 SetSelectionDirection(eDirNext
);
498 mOffsetYToCaretLogicalPosition
=
499 mActiveCaret
->LogicalPosition().y
- aPoint
.y
;
500 SetSelectionDragState(true);
501 DispatchCaretStateChangedEvent(CaretChangedReason::Presscaret
, &aPoint
);
508 nsresult
AccessibleCaretManager::DragCaret(const nsPoint
& aPoint
) {
509 MOZ_ASSERT(mActiveCaret
);
510 MOZ_ASSERT(GetCaretMode() != CaretMode::None
);
512 if (!mPresShell
|| !mPresShell
->GetRootFrame() || !GetSelection()) {
513 return NS_ERROR_NULL_POINTER
;
516 StopSelectionAutoScrollTimer();
517 DragCaretInternal(aPoint
);
519 // We want to scroll the page even if we failed to drag the caret.
520 StartSelectionAutoScrollTimer(aPoint
);
523 if (StaticPrefs::layout_accessiblecaret_magnifier_enabled()) {
524 DispatchCaretStateChangedEvent(CaretChangedReason::Dragcaret
, &aPoint
);
529 nsresult
AccessibleCaretManager::ReleaseCaret() {
530 MOZ_ASSERT(mActiveCaret
);
532 mActiveCaret
= nullptr;
533 SetSelectionDragState(false);
534 mDesiredAsyncPanZoomState
.Update(*this);
535 DispatchCaretStateChangedEvent(CaretChangedReason::Releasecaret
);
539 nsresult
AccessibleCaretManager::TapCaret(const nsPoint
& aPoint
) {
540 MOZ_ASSERT(GetCaretMode() != CaretMode::None
);
542 nsresult rv
= NS_ERROR_FAILURE
;
544 if (GetCaretMode() == CaretMode::Cursor
) {
545 DispatchCaretStateChangedEvent(CaretChangedReason::Taponcaret
, &aPoint
);
552 static EnumSet
<nsLayoutUtils::FrameForPointOption
> GetHitTestOptions() {
553 EnumSet
<nsLayoutUtils::FrameForPointOption
> options
= {
554 nsLayoutUtils::FrameForPointOption::IgnorePaintSuppression
,
555 nsLayoutUtils::FrameForPointOption::IgnoreCrossDoc
};
559 nsresult
AccessibleCaretManager::SelectWordOrShortcut(const nsPoint
& aPoint
) {
560 // If the long-tap is landing on a pre-existing selection, don't replace
561 // it with a new one. Instead just return and let the context menu pop up
562 // on the pre-existing selection.
563 if (GetCaretMode() == CaretMode::Selection
&&
564 GetSelection()->ContainsPoint(aPoint
)) {
565 AC_LOG("%s: UpdateCarets() for current selection", __FUNCTION__
);
567 ProvideHapticFeedback();
572 return NS_ERROR_UNEXPECTED
;
575 nsIFrame
* rootFrame
= mPresShell
->GetRootFrame();
577 return NS_ERROR_NOT_AVAILABLE
;
580 // Find the frame under point.
581 AutoWeakFrame ptFrame
= nsLayoutUtils::GetFrameForPoint(
582 RelativeTo
{rootFrame
}, aPoint
, GetHitTestOptions());
583 if (!ptFrame
.GetFrame()) {
584 return NS_ERROR_FAILURE
;
587 nsIFrame
* focusableFrame
= GetFocusableFrame(ptFrame
);
589 #ifdef DEBUG_FRAME_DUMP
590 AC_LOG("%s: Found %s under (%d, %d)", __FUNCTION__
, ptFrame
->ListTag().get(),
592 AC_LOG("%s: Found %s focusable", __FUNCTION__
,
593 focusableFrame
? focusableFrame
->ListTag().get() : "no frame");
596 // Get ptInFrame here so that we don't need to check whether rootFrame is
597 // alive later. Note that if ptFrame is being moved by
598 // IMEStateManager::NotifyIME() or ChangeFocusToOrClearOldFocus() below,
599 // something under the original point will be selected, which may not be the
600 // original text the user wants to select.
601 nsPoint ptInFrame
= aPoint
;
602 nsLayoutUtils::TransformPoint(RelativeTo
{rootFrame
}, RelativeTo
{ptFrame
},
605 // Firstly check long press on an empty editable content.
606 Element
* newFocusEditingHost
= GetEditingHostForFrame(ptFrame
);
607 if (focusableFrame
&& newFocusEditingHost
&&
608 !HasNonEmptyTextContent(newFocusEditingHost
)) {
609 ChangeFocusToOrClearOldFocus(focusableFrame
);
612 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
613 mCarets
.GetFirst()->SetAppearance(Appearance::Normal
);
615 // We need to update carets to get correct information before dispatching
616 // CaretStateChangedEvent.
618 ProvideHapticFeedback();
619 DispatchCaretStateChangedEvent(CaretChangedReason::Longpressonemptycontent
);
623 bool selectable
= ptFrame
->IsSelectable(nullptr);
625 #ifdef DEBUG_FRAME_DUMP
626 AC_LOG("%s: %s %s selectable.", __FUNCTION__
, ptFrame
->ListTag().get(),
627 selectable
? "is" : "is NOT");
631 return NS_ERROR_FAILURE
;
634 // Commit the composition string of the old editable focus element (if there
635 // is any) before changing the focus.
636 IMEStateManager::NotifyIME(widget::REQUEST_TO_COMMIT_COMPOSITION
,
637 mPresShell
->GetPresContext());
638 if (!ptFrame
.IsAlive()) {
639 // Cannot continue because ptFrame died.
640 return NS_ERROR_FAILURE
;
643 // ptFrame is selectable. Now change the focus.
644 ChangeFocusToOrClearOldFocus(focusableFrame
);
645 if (!ptFrame
.IsAlive()) {
646 // Cannot continue because ptFrame died.
647 return NS_ERROR_FAILURE
;
650 // If long tap point isn't selectable frame for caret and frame selection
651 // can find a better frame for caret, we don't select a word.
652 // See https://webcompat.com/issues/15953
653 nsIFrame::ContentOffsets offsets
=
654 ptFrame
->GetContentOffsetsFromPoint(ptInFrame
, nsIFrame::SKIP_HIDDEN
);
655 if (offsets
.content
) {
656 RefPtr
<nsFrameSelection
> frameSelection
= GetFrameSelection();
657 if (frameSelection
) {
659 nsIFrame
* theFrame
= nsFrameSelection::GetFrameForNodeOffset(
660 offsets
.content
, offsets
.offset
, offsets
.associate
, &offset
);
661 if (theFrame
&& theFrame
!= ptFrame
) {
662 SetSelectionDragState(true);
663 frameSelection
->HandleClick(
664 MOZ_KnownLive(offsets
.content
) /* bug 1636889 */,
665 offsets
.StartOffset(), offsets
.EndOffset(),
666 nsFrameSelection::FocusMode::kCollapseToNewPoint
,
668 SetSelectionDragState(false);
669 ClearMaintainedSelection();
672 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
673 mCarets
.GetFirst()->SetAppearance(Appearance::Normal
);
677 ProvideHapticFeedback();
678 DispatchCaretStateChangedEvent(
679 CaretChangedReason::Longpressonemptycontent
);
686 // Then try select a word under point.
687 nsresult rv
= SelectWord(ptFrame
, ptInFrame
);
689 ProvideHapticFeedback();
694 void AccessibleCaretManager::OnScrollStart() {
695 AC_LOG("%s", __FUNCTION__
);
697 nsAutoScriptBlocker scriptBlocker
;
698 AutoRestore
<bool> saveAllowFlushingLayout(mLayoutFlusher
.mAllowFlushing
);
699 mLayoutFlusher
.mAllowFlushing
= false;
701 Maybe
<PresShell::AutoAssertNoFlush
> assert;
703 assert.emplace(*mPresShell
);
706 mIsScrollStarted
= true;
708 if (mCarets
.HasLogicallyVisibleCaret()) {
709 // Dispatch the event only if one of the carets is logically visible like in
710 // HideCaretsAndDispatchCaretStateChangedEvent().
711 DispatchCaretStateChangedEvent(CaretChangedReason::Scroll
);
715 void AccessibleCaretManager::OnScrollEnd() {
716 nsAutoScriptBlocker scriptBlocker
;
717 AutoRestore
<bool> saveAllowFlushingLayout(mLayoutFlusher
.mAllowFlushing
);
718 mLayoutFlusher
.mAllowFlushing
= false;
720 Maybe
<PresShell::AutoAssertNoFlush
> assert;
722 assert.emplace(*mPresShell
);
725 mIsScrollStarted
= false;
727 if (GetCaretMode() == CaretMode::Cursor
) {
728 if (!mCarets
.GetFirst()->IsLogicallyVisible()) {
729 // If the caret is hidden (Appearance::None) due to blur, no
730 // need to update it.
735 // For mouse and keyboard input, we don't want to show the carets.
736 if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
737 (mLastInputSource
== MouseEvent_Binding::MOZ_SOURCE_MOUSE
||
738 mLastInputSource
== MouseEvent_Binding::MOZ_SOURCE_KEYBOARD
)) {
739 AC_LOG("%s: HideCaretsAndDispatchCaretStateChangedEvent()", __FUNCTION__
);
740 HideCaretsAndDispatchCaretStateChangedEvent();
744 AC_LOG("%s: UpdateCarets()", __FUNCTION__
);
748 void AccessibleCaretManager::OnScrollPositionChanged() {
749 nsAutoScriptBlocker scriptBlocker
;
750 AutoRestore
<bool> saveAllowFlushingLayout(mLayoutFlusher
.mAllowFlushing
);
751 mLayoutFlusher
.mAllowFlushing
= false;
753 Maybe
<PresShell::AutoAssertNoFlush
> assert;
755 assert.emplace(*mPresShell
);
758 if (mCarets
.HasLogicallyVisibleCaret()) {
759 if (mIsScrollStarted
) {
760 // We don't want extra CaretStateChangedEvents dispatched when user is
761 // scrolling the page.
762 AC_LOG("%s: UpdateCarets(RespectOldAppearance | DispatchNoEvent)",
764 UpdateCarets({UpdateCaretsHint::RespectOldAppearance
,
765 UpdateCaretsHint::DispatchNoEvent
});
767 AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__
);
768 UpdateCarets(UpdateCaretsHint::RespectOldAppearance
);
773 void AccessibleCaretManager::OnReflow() {
774 nsAutoScriptBlocker scriptBlocker
;
775 AutoRestore
<bool> saveAllowFlushingLayout(mLayoutFlusher
.mAllowFlushing
);
776 mLayoutFlusher
.mAllowFlushing
= false;
778 Maybe
<PresShell::AutoAssertNoFlush
> assert;
780 assert.emplace(*mPresShell
);
783 if (mCarets
.HasLogicallyVisibleCaret()) {
784 AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__
);
785 UpdateCarets(UpdateCaretsHint::RespectOldAppearance
);
789 void AccessibleCaretManager::OnBlur() {
790 AC_LOG("%s: HideCaretsAndDispatchCaretStateChangedEvent()", __FUNCTION__
);
791 HideCaretsAndDispatchCaretStateChangedEvent();
794 void AccessibleCaretManager::OnKeyboardEvent() {
795 if (GetCaretMode() == CaretMode::Cursor
) {
796 AC_LOG("%s: HideCaretsAndDispatchCaretStateChangedEvent()", __FUNCTION__
);
797 HideCaretsAndDispatchCaretStateChangedEvent();
801 void AccessibleCaretManager::SetLastInputSource(uint16_t aInputSource
) {
802 mLastInputSource
= aInputSource
;
805 bool AccessibleCaretManager::ShouldDisableApz() const {
806 return mDesiredAsyncPanZoomState
.Get() ==
807 DesiredAsyncPanZoomState::Value::Disabled
;
810 Selection
* AccessibleCaretManager::GetSelection() const {
811 RefPtr
<nsFrameSelection
> fs
= GetFrameSelection();
815 return fs
->GetSelection(SelectionType::eNormal
);
818 already_AddRefed
<nsFrameSelection
> AccessibleCaretManager::GetFrameSelection()
824 // Prevent us from touching the nsFrameSelection associated with other
826 RefPtr
<nsFrameSelection
> fs
= mPresShell
->GetLastFocusedFrameSelection();
827 if (!fs
|| fs
->GetPresShell() != mPresShell
) {
834 nsAutoString
AccessibleCaretManager::StringifiedSelection() const {
836 RefPtr
<Selection
> selection
= GetSelection();
838 selection
->Stringify(str
, mLayoutFlusher
.mAllowFlushing
839 ? Selection::FlushFrames::Yes
840 : Selection::FlushFrames::No
);
846 Element
* AccessibleCaretManager::GetEditingHostForFrame(
847 const nsIFrame
* aFrame
) {
852 auto content
= aFrame
->GetContent();
857 return content
->GetEditingHost();
860 AccessibleCaretManager::CaretMode
AccessibleCaretManager::GetCaretMode() const {
861 const Selection
* selection
= GetSelection();
863 return CaretMode::None
;
866 const uint32_t rangeCount
= selection
->RangeCount();
867 if (rangeCount
<= 0) {
868 return CaretMode::None
;
871 const nsFocusManager
* fm
= nsFocusManager::GetFocusManager();
873 if (fm
->GetFocusedWindow() != mPresShell
->GetDocument()->GetWindow()) {
874 // Hide carets if the window is not focused.
875 return CaretMode::None
;
878 if (selection
->IsCollapsed()) {
879 return CaretMode::Cursor
;
882 return CaretMode::Selection
;
885 nsIFrame
* AccessibleCaretManager::GetFocusableFrame(nsIFrame
* aFrame
) const {
886 // This implementation is similar to EventStateManager::PostHandleEvent().
887 // Look for the nearest enclosing focusable frame.
888 nsIFrame
* focusableFrame
= aFrame
;
889 while (focusableFrame
) {
890 if (focusableFrame
->IsFocusable(/* aWithMouse = */ true)) {
893 focusableFrame
= focusableFrame
->GetParent();
895 return focusableFrame
;
898 void AccessibleCaretManager::ChangeFocusToOrClearOldFocus(
899 nsIFrame
* aFrame
) const {
900 RefPtr
<nsFocusManager
> fm
= nsFocusManager::GetFocusManager();
904 nsIContent
* focusableContent
= aFrame
->GetContent();
905 MOZ_ASSERT(focusableContent
, "Focusable frame must have content!");
906 RefPtr
<Element
> focusableElement
= Element::FromNode(focusableContent
);
907 fm
->SetFocus(focusableElement
, nsIFocusManager::FLAG_BYLONGPRESS
);
908 } else if (nsCOMPtr
<nsPIDOMWindowOuter
> win
=
909 mPresShell
->GetDocument()->GetWindow()) {
911 fm
->SetFocusedWindow(win
);
915 nsresult
AccessibleCaretManager::SelectWord(nsIFrame
* aFrame
,
916 const nsPoint
& aPoint
) const {
917 AC_LOGV("%s", __FUNCTION__
);
919 SetSelectionDragState(true);
920 const RefPtr
<nsPresContext
> pinnedPresContext
{mPresShell
->GetPresContext()};
921 nsresult rs
= aFrame
->SelectByTypeAtPoint(pinnedPresContext
, aPoint
,
922 eSelectWord
, eSelectWord
, 0);
924 SetSelectionDragState(false);
925 ClearMaintainedSelection();
927 // Smart-select phone numbers if possible.
928 if (StaticPrefs::layout_accessiblecaret_extend_selection_for_phone_number()) {
929 SelectMoreIfPhoneNumber();
935 void AccessibleCaretManager::SetSelectionDragState(bool aState
) const {
936 RefPtr
<nsFrameSelection
> fs
= GetFrameSelection();
938 fs
->SetDragState(aState
);
942 bool AccessibleCaretManager::IsPhoneNumber(const nsAString
& aCandidate
) const {
943 RefPtr
<Document
> doc
= mPresShell
->GetDocument();
944 nsAutoString
phoneNumberRegex(u
"(^\\+)?[0-9 ,\\-.\\(\\)*#pw]{1,30}$"_ns
);
945 return nsContentUtils::IsPatternMatching(aCandidate
,
946 std::move(phoneNumberRegex
), doc
)
950 void AccessibleCaretManager::SelectMoreIfPhoneNumber() const {
951 if (IsPhoneNumber(StringifiedSelection())) {
952 SetSelectionDirection(eDirNext
);
953 ExtendPhoneNumberSelection(u
"forward"_ns
);
955 SetSelectionDirection(eDirPrevious
);
956 ExtendPhoneNumberSelection(u
"backward"_ns
);
958 SetSelectionDirection(eDirNext
);
962 void AccessibleCaretManager::ExtendPhoneNumberSelection(
963 const nsAString
& aDirection
) const {
968 // Extend the phone number selection until we find a boundary.
969 RefPtr
<Selection
> selection
= GetSelection();
972 const nsRange
* anchorFocusRange
= selection
->GetAnchorFocusRange();
973 if (!anchorFocusRange
) {
977 // Backup the anchor focus range since both anchor node and focus node might
978 // be changed after calling Selection::Modify().
979 RefPtr
<nsRange
> oldAnchorFocusRange
= anchorFocusRange
->CloneRange();
981 // Save current focus node, focus offset and the selected text so that
982 // we can compare them with the modified ones later.
983 nsINode
* oldFocusNode
= selection
->GetFocusNode();
984 uint32_t oldFocusOffset
= selection
->FocusOffset();
985 nsAutoString oldSelectedText
= StringifiedSelection();
987 // Extend the selection by one char.
988 selection
->Modify(u
"extend"_ns
, aDirection
, u
"character"_ns
,
990 if (IsTerminated() == Terminated::Yes
) {
994 // If the selection didn't change, (can't extend further), we're done.
995 if (selection
->GetFocusNode() == oldFocusNode
&&
996 selection
->FocusOffset() == oldFocusOffset
) {
1000 // If the changed selection isn't a valid phone number, we're done.
1001 // Also, if the selection was extended to a new block node, the string
1002 // returned by stringify() won't have a new line at the beginning or the
1003 // end of the string. Therefore, if either focus node or offset is
1004 // changed, but selected text is not changed, we're done, too.
1005 nsAutoString selectedText
= StringifiedSelection();
1007 if (!IsPhoneNumber(selectedText
) || oldSelectedText
== selectedText
) {
1008 // Backout the undesired selection extend, restore the old anchor focus
1009 // range before exit.
1010 selection
->SetAnchorFocusToRange(oldAnchorFocusRange
);
1016 void AccessibleCaretManager::SetSelectionDirection(nsDirection aDir
) const {
1017 Selection
* selection
= GetSelection();
1019 selection
->AdjustAnchorFocusForMultiRange(aDir
);
1023 void AccessibleCaretManager::ClearMaintainedSelection() const {
1024 // Selection made by double-clicking for example will maintain the original
1025 // word selection. We should clear it so that we can drag caret freely.
1026 RefPtr
<nsFrameSelection
> fs
= GetFrameSelection();
1028 fs
->MaintainSelection(eSelectNoAmount
);
1032 void AccessibleCaretManager::LayoutFlusher::MaybeFlush(
1033 const PresShell
& aPresShell
) {
1034 if (mAllowFlushing
) {
1035 AutoRestore
<bool> flushing(mFlushing
);
1038 if (Document
* doc
= aPresShell
.GetDocument()) {
1039 doc
->FlushPendingNotifications(FlushType::Layout
);
1040 // Don't access the PresShell after flushing, it could've become invalid.
1045 nsIFrame
* AccessibleCaretManager::GetFrameForFirstRangeStartOrLastRangeEnd(
1046 nsDirection aDirection
, int32_t* aOutOffset
, nsIContent
** aOutContent
,
1047 int32_t* aOutContentOffset
) const {
1052 MOZ_ASSERT(GetCaretMode() == CaretMode::Selection
);
1053 MOZ_ASSERT(aOutOffset
, "aOutOffset shouldn't be nullptr!");
1055 const nsRange
* range
= nullptr;
1056 RefPtr
<nsINode
> startNode
;
1057 RefPtr
<nsINode
> endNode
;
1058 int32_t nodeOffset
= 0;
1059 CaretAssociationHint hint
;
1061 RefPtr
<Selection
> selection
= GetSelection();
1062 bool findInFirstRangeStart
= aDirection
== eDirNext
;
1064 if (findInFirstRangeStart
) {
1065 range
= selection
->GetRangeAt(0);
1066 startNode
= range
->GetStartContainer();
1067 endNode
= range
->GetEndContainer();
1068 nodeOffset
= range
->StartOffset();
1069 hint
= CARET_ASSOCIATE_AFTER
;
1071 MOZ_ASSERT(selection
->RangeCount() > 0);
1072 range
= selection
->GetRangeAt(selection
->RangeCount() - 1);
1073 startNode
= range
->GetEndContainer();
1074 endNode
= range
->GetStartContainer();
1075 nodeOffset
= range
->EndOffset();
1076 hint
= CARET_ASSOCIATE_BEFORE
;
1079 nsCOMPtr
<nsIContent
> startContent
= do_QueryInterface(startNode
);
1080 nsIFrame
* startFrame
= nsFrameSelection::GetFrameForNodeOffset(
1081 startContent
, nodeOffset
, hint
, aOutOffset
);
1085 RefPtr
<TreeWalker
> walker
= mPresShell
->GetDocument()->CreateTreeWalker(
1086 *startNode
, dom::NodeFilter_Binding::SHOW_ALL
, nullptr, err
);
1092 startFrame
= startContent
? startContent
->GetPrimaryFrame() : nullptr;
1093 while (!startFrame
&& startNode
!= endNode
) {
1094 startNode
= findInFirstRangeStart
? walker
->NextNode(err
)
1095 : walker
->PreviousNode(err
);
1101 startContent
= startNode
->AsContent();
1102 startFrame
= startContent
? startContent
->GetPrimaryFrame() : nullptr;
1105 // We are walking among the nodes in the content tree, so the node offset
1106 // relative to startNode should be set to 0.
1113 startContent
.forget(aOutContent
);
1115 if (aOutContentOffset
) {
1116 *aOutContentOffset
= nodeOffset
;
1123 bool AccessibleCaretManager::RestrictCaretDraggingOffsets(
1124 nsIFrame::ContentOffsets
& aOffsets
) {
1129 MOZ_ASSERT(GetCaretMode() == CaretMode::Selection
);
1132 mActiveCaret
== mCarets
.GetFirst() ? eDirPrevious
: eDirNext
;
1134 nsCOMPtr
<nsIContent
> content
;
1135 int32_t contentOffset
= 0;
1136 nsIFrame
* frame
= GetFrameForFirstRangeStartOrLastRangeEnd(
1137 dir
, &offset
, getter_AddRefs(content
), &contentOffset
);
1143 // Compare the active caret's new position (aOffsets) to the inactive caret's
1145 NS_ASSERTION(contentOffset
>= 0, "contentOffset should not be negative");
1146 const Maybe
<int32_t> cmpToInactiveCaretPos
=
1147 nsContentUtils::ComparePoints_AllowNegativeOffsets(
1148 aOffsets
.content
, aOffsets
.StartOffset(), content
, contentOffset
);
1149 if (NS_WARN_IF(!cmpToInactiveCaretPos
)) {
1150 // Potentially handle this properly when Selection across Shadow DOM
1151 // boundary is implemented
1152 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1156 // Move one character (in the direction of dir) from the inactive caret's
1157 // position. This is the limit for the active caret's new position.
1158 PeekOffsetStruct
limit(
1159 eSelectCluster
, dir
, offset
, nsPoint(0, 0),
1160 {PeekOffsetOption::JumpLines
, PeekOffsetOption::ScrollViewStop
});
1161 nsresult rv
= frame
->PeekOffset(&limit
);
1162 if (NS_FAILED(rv
)) {
1163 limit
.mResultContent
= content
;
1164 limit
.mContentOffset
= contentOffset
;
1167 // Compare the active caret's new position (aOffsets) to the limit.
1168 NS_ASSERTION(limit
.mContentOffset
>= 0,
1169 "limit.mContentOffset should not be negative");
1170 const Maybe
<int32_t> cmpToLimit
=
1171 nsContentUtils::ComparePoints_AllowNegativeOffsets(
1172 aOffsets
.content
, aOffsets
.StartOffset(), limit
.mResultContent
,
1173 limit
.mContentOffset
);
1174 if (NS_WARN_IF(!cmpToLimit
)) {
1175 // Potentially handle this properly when Selection across Shadow DOM
1176 // boundary is implemented
1177 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1181 auto SetOffsetsToLimit
= [&aOffsets
, &limit
]() {
1182 aOffsets
.content
= limit
.mResultContent
;
1183 aOffsets
.offset
= limit
.mContentOffset
;
1184 aOffsets
.secondaryOffset
= limit
.mContentOffset
;
1188 layout_accessiblecaret_allow_dragging_across_other_caret()) {
1189 if ((mActiveCaret
== mCarets
.GetFirst() && *cmpToLimit
== 1) ||
1190 (mActiveCaret
== mCarets
.GetSecond() && *cmpToLimit
== -1)) {
1191 // The active caret's position is past the limit, which we don't allow
1192 // here. So set it to the limit, resulting in one character being
1194 SetOffsetsToLimit();
1197 switch (*cmpToInactiveCaretPos
) {
1199 // The active caret's position is the same as the position of the
1200 // inactive caret. So set it to the limit to prevent the selection from
1201 // being collapsed, resulting in one character being selected.
1202 SetOffsetsToLimit();
1205 if (mActiveCaret
== mCarets
.GetFirst()) {
1206 // First caret was moved across the second caret. After making change
1207 // to the selection, the user will drag the second caret.
1208 mActiveCaret
= mCarets
.GetSecond();
1212 if (mActiveCaret
== mCarets
.GetSecond()) {
1213 // Second caret was moved across the first caret. After making change
1214 // to the selection, the user will drag the first caret.
1215 mActiveCaret
= mCarets
.GetFirst();
1224 bool AccessibleCaretManager::CompareTreePosition(nsIFrame
* aStartFrame
,
1225 nsIFrame
* aEndFrame
) const {
1226 return (aStartFrame
&& aEndFrame
&&
1227 nsLayoutUtils::CompareTreePosition(aStartFrame
, aEndFrame
) <= 0);
1230 nsresult
AccessibleCaretManager::DragCaretInternal(const nsPoint
& aPoint
) {
1231 MOZ_ASSERT(mPresShell
);
1233 nsIFrame
* rootFrame
= mPresShell
->GetRootFrame();
1234 MOZ_ASSERT(rootFrame
, "We need root frame to compute caret dragging!");
1236 nsPoint point
= AdjustDragBoundary(
1237 nsPoint(aPoint
.x
, aPoint
.y
+ mOffsetYToCaretLogicalPosition
));
1239 // Find out which content we point to
1241 nsIFrame
* ptFrame
= nsLayoutUtils::GetFrameForPoint(
1242 RelativeTo
{rootFrame
}, point
, GetHitTestOptions());
1244 return NS_ERROR_FAILURE
;
1247 RefPtr
<nsFrameSelection
> fs
= GetFrameSelection();
1251 nsIFrame
* newFrame
= nullptr;
1253 nsPoint ptInFrame
= point
;
1254 nsLayoutUtils::TransformPoint(RelativeTo
{rootFrame
}, RelativeTo
{ptFrame
},
1256 result
= fs
->ConstrainFrameAndPointToAnchorSubtree(ptFrame
, ptInFrame
,
1257 &newFrame
, newPoint
);
1258 if (NS_FAILED(result
) || !newFrame
) {
1259 return NS_ERROR_FAILURE
;
1262 if (!newFrame
->IsSelectable(nullptr)) {
1263 return NS_ERROR_FAILURE
;
1266 nsIFrame::ContentOffsets offsets
=
1267 newFrame
->GetContentOffsetsFromPoint(newPoint
);
1268 if (offsets
.IsNull()) {
1269 return NS_ERROR_FAILURE
;
1272 if (GetCaretMode() == CaretMode::Selection
&&
1273 !RestrictCaretDraggingOffsets(offsets
)) {
1274 return NS_ERROR_FAILURE
;
1277 ClearMaintainedSelection();
1279 const nsFrameSelection::FocusMode focusMode
=
1280 (GetCaretMode() == CaretMode::Selection
)
1281 ? nsFrameSelection::FocusMode::kExtendSelection
1282 : nsFrameSelection::FocusMode::kCollapseToNewPoint
;
1283 fs
->HandleClick(MOZ_KnownLive(offsets
.content
) /* bug 1636889 */,
1284 offsets
.StartOffset(), offsets
.EndOffset(), focusMode
,
1290 nsRect
AccessibleCaretManager::GetAllChildFrameRectsUnion(nsIFrame
* aFrame
) {
1293 // Drill through scroll frames, we don't want to include scrollbar child
1295 for (nsIFrame
* frame
= aFrame
->GetContentInsertionFrame(); frame
;
1296 frame
= frame
->GetNextContinuation()) {
1299 for (const auto& childList
: frame
->ChildLists()) {
1300 // Loop all children to union their scrollable overflow rect.
1301 for (nsIFrame
* child
: childList
.mList
) {
1302 nsRect childRect
= child
->ScrollableOverflowRectRelativeToSelf();
1303 nsLayoutUtils::TransformRect(child
, frame
, childRect
);
1305 // A TextFrame containing only '\n' has positive height and width 0, or
1306 // positive width and height 0 if it's vertical. Need to use UnionEdges
1307 // to add its rect. BRFrame rect should be non-empty.
1308 if (childRect
.IsEmpty()) {
1309 frameRect
= frameRect
.UnionEdges(childRect
);
1311 frameRect
= frameRect
.Union(childRect
);
1316 MOZ_ASSERT(!frameRect
.IsEmpty(),
1317 "Editable frames should have at least one BRFrame child to make "
1318 "frameRect non-empty!");
1319 if (frame
!= aFrame
) {
1320 nsLayoutUtils::TransformRect(frame
, aFrame
, frameRect
);
1322 unionRect
= unionRect
.Union(frameRect
);
1328 nsPoint
AccessibleCaretManager::AdjustDragBoundary(
1329 const nsPoint
& aPoint
) const {
1330 nsPoint adjustedPoint
= aPoint
;
1332 int32_t focusOffset
= 0;
1333 nsIFrame
* focusFrame
=
1334 nsCaret::GetFrameAndOffset(GetSelection(), nullptr, 0, &focusOffset
);
1335 Element
* editingHost
= GetEditingHostForFrame(focusFrame
);
1338 nsIFrame
* editingHostFrame
= editingHost
->GetPrimaryFrame();
1339 if (editingHostFrame
) {
1341 AccessibleCaretManager::GetAllChildFrameRectsUnion(editingHostFrame
);
1342 nsLayoutUtils::TransformRect(editingHostFrame
, mPresShell
->GetRootFrame(),
1345 // Shrink the rect to make sure we never hit the boundary.
1346 boundary
.Deflate(kBoundaryAppUnits
);
1348 adjustedPoint
= boundary
.ClampPoint(adjustedPoint
);
1352 if (GetCaretMode() == CaretMode::Selection
&&
1354 layout_accessiblecaret_allow_dragging_across_other_caret()) {
1355 // Bug 1068474: Adjust the Y-coordinate so that the carets won't be in tilt
1356 // mode when a caret is being dragged surpass the other caret.
1358 // For example, when dragging the second caret, the horizontal boundary
1359 // (lower bound) of its Y-coordinate is the logical position of the first
1360 // caret. Likewise, when dragging the first caret, the horizontal boundary
1361 // (upper bound) of its Y-coordinate is the logical position of the second
1363 if (mActiveCaret
== mCarets
.GetFirst()) {
1364 nscoord dragDownBoundaryY
= mCarets
.GetSecond()->LogicalPosition().y
;
1365 if (dragDownBoundaryY
> 0 && adjustedPoint
.y
> dragDownBoundaryY
) {
1366 adjustedPoint
.y
= dragDownBoundaryY
;
1369 nscoord dragUpBoundaryY
= mCarets
.GetFirst()->LogicalPosition().y
;
1370 if (adjustedPoint
.y
< dragUpBoundaryY
) {
1371 adjustedPoint
.y
= dragUpBoundaryY
;
1376 return adjustedPoint
;
1379 void AccessibleCaretManager::StartSelectionAutoScrollTimer(
1380 const nsPoint
& aPoint
) const {
1381 Selection
* selection
= GetSelection();
1382 MOZ_ASSERT(selection
);
1384 nsIFrame
* anchorFrame
= selection
->GetPrimaryFrameForAnchorNode();
1389 nsIScrollableFrame
* scrollFrame
= nsLayoutUtils::GetNearestScrollableFrame(
1390 anchorFrame
, nsLayoutUtils::SCROLLABLE_SAME_DOC
|
1391 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN
);
1396 nsIFrame
* capturingFrame
= scrollFrame
->GetScrolledFrame();
1397 if (!capturingFrame
) {
1401 nsIFrame
* rootFrame
= mPresShell
->GetRootFrame();
1402 MOZ_ASSERT(rootFrame
);
1403 nsPoint ptInScrolled
= aPoint
;
1404 nsLayoutUtils::TransformPoint(RelativeTo
{rootFrame
},
1405 RelativeTo
{capturingFrame
}, ptInScrolled
);
1407 RefPtr
<nsFrameSelection
> fs
= GetFrameSelection();
1409 fs
->StartAutoScrollTimer(capturingFrame
, ptInScrolled
, kAutoScrollTimerDelay
);
1412 void AccessibleCaretManager::StopSelectionAutoScrollTimer() const {
1413 RefPtr
<nsFrameSelection
> fs
= GetFrameSelection();
1415 fs
->StopAutoScrollTimer();
1418 void AccessibleCaretManager::DispatchCaretStateChangedEvent(
1419 CaretChangedReason aReason
, const nsPoint
* aPoint
) {
1420 if (MaybeFlushLayout() == Terminated::Yes
) {
1424 const Selection
* sel
= GetSelection();
1429 Document
* doc
= mPresShell
->GetDocument();
1432 CaretStateChangedEventInit init
;
1433 init
.mBubbles
= true;
1435 const nsRange
* range
= sel
->GetAnchorFocusRange();
1436 nsINode
* commonAncestorNode
= nullptr;
1438 commonAncestorNode
= range
->GetClosestCommonInclusiveAncestor();
1441 if (!commonAncestorNode
) {
1442 commonAncestorNode
= sel
->GetFrameSelection()->GetAncestorLimiter();
1445 RefPtr
<DOMRect
> domRect
= new DOMRect(ToSupports(doc
));
1446 nsRect rect
= nsLayoutUtils::GetSelectionBoundingRect(sel
);
1448 nsIFrame
* commonAncestorFrame
= nullptr;
1449 nsIFrame
* rootFrame
= mPresShell
->GetRootFrame();
1451 if (commonAncestorNode
&& commonAncestorNode
->IsContent()) {
1452 commonAncestorFrame
= commonAncestorNode
->AsContent()->GetPrimaryFrame();
1455 if (commonAncestorFrame
&& rootFrame
) {
1456 nsLayoutUtils::TransformRect(rootFrame
, commonAncestorFrame
, rect
);
1457 nsRect clampedRect
=
1458 nsLayoutUtils::ClampRectToScrollFrames(commonAncestorFrame
, rect
);
1459 nsLayoutUtils::TransformRect(commonAncestorFrame
, rootFrame
, clampedRect
);
1461 init
.mSelectionVisible
= !clampedRect
.IsEmpty();
1463 init
.mSelectionVisible
= true;
1466 domRect
->SetLayoutRect(rect
);
1468 // Send isEditable info w/ event detail. This info can help determine
1469 // whether to show cut command on selection dialog or not.
1470 init
.mSelectionEditable
=
1471 commonAncestorFrame
&& GetEditingHostForFrame(commonAncestorFrame
);
1473 init
.mBoundingClientRect
= domRect
;
1474 init
.mReason
= aReason
;
1475 init
.mCollapsed
= sel
->IsCollapsed();
1476 init
.mCaretVisible
= mCarets
.HasLogicallyVisibleCaret();
1477 init
.mCaretVisuallyVisible
= mCarets
.HasVisuallyVisibleCaret();
1478 init
.mSelectedTextContent
= StringifiedSelection();
1481 CSSIntPoint pt
= CSSPixel::FromAppUnitsRounded(*aPoint
);
1482 init
.mClientX
= pt
.x
;
1483 init
.mClientY
= pt
.y
;
1486 RefPtr
<CaretStateChangedEvent
> event
= CaretStateChangedEvent::Constructor(
1487 doc
, u
"mozcaretstatechanged"_ns
, init
);
1488 event
->SetTrusted(true);
1490 AC_LOG("%s: reason %" PRIu32
", collapsed %d, caretVisible %" PRIu32
,
1491 __FUNCTION__
, static_cast<uint32_t>(init
.mReason
), init
.mCollapsed
,
1492 static_cast<uint32_t>(init
.mCaretVisible
));
1494 (new AsyncEventDispatcher(doc
, event
.forget(), ChromeOnlyDispatch::eYes
))
1498 AccessibleCaretManager::Carets::Carets(UniquePtr
<AccessibleCaret
> aFirst
,
1499 UniquePtr
<AccessibleCaret
> aSecond
)
1500 : mFirst
{std::move(aFirst
)}, mSecond
{std::move(aSecond
)} {}
1502 } // namespace mozilla