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 "AccessibleCaret.h"
10 #include "AccessibleCaretEventHub.h"
11 #include "AccessibleCaretLogger.h"
12 #include "mozilla/AsyncEventDispatcher.h"
13 #include "mozilla/AutoRestore.h"
14 #include "mozilla/dom/Element.h"
15 #include "mozilla/dom/MouseEventBinding.h"
16 #include "mozilla/dom/NodeFilterBinding.h"
17 #include "mozilla/dom/Selection.h"
18 #include "mozilla/dom/TreeWalker.h"
19 #include "mozilla/IMEStateManager.h"
20 #include "mozilla/IntegerPrintfMacros.h"
21 #include "mozilla/PresShell.h"
22 #include "mozilla/StaticPrefs_layout.h"
24 #include "nsContainerFrame.h"
25 #include "nsContentUtils.h"
27 #include "nsFocusManager.h"
29 #include "nsFrameSelection.h"
30 #include "nsGenericHTMLElement.h"
31 #include "nsIHapticFeedback.h"
36 #define AC_LOG(message, ...) \
37 AC_LOG_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
40 #define AC_LOGV(message, ...) \
41 AC_LOGV_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
44 using Appearance
= AccessibleCaret::Appearance
;
45 using PositionChangedResult
= AccessibleCaret::PositionChangedResult
;
47 #define AC_PROCESS_ENUM_TO_STREAM(e) \
51 std::ostream
& operator<<(std::ostream
& aStream
,
52 const AccessibleCaretManager::CaretMode
& aCaretMode
) {
53 using CaretMode
= AccessibleCaretManager::CaretMode
;
55 AC_PROCESS_ENUM_TO_STREAM(CaretMode::None
);
56 AC_PROCESS_ENUM_TO_STREAM(CaretMode::Cursor
);
57 AC_PROCESS_ENUM_TO_STREAM(CaretMode::Selection
);
62 std::ostream
& operator<<(
63 std::ostream
& aStream
,
64 const AccessibleCaretManager::UpdateCaretsHint
& aHint
) {
65 using UpdateCaretsHint
= AccessibleCaretManager::UpdateCaretsHint
;
67 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::Default
);
68 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::RespectOldAppearance
);
69 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::DispatchNoEvent
);
73 #undef AC_PROCESS_ENUM_TO_STREAM
75 AccessibleCaretManager::AccessibleCaretManager(PresShell
* aPresShell
)
76 : mPresShell(aPresShell
) {
81 mFirstCaret
= MakeUnique
<AccessibleCaret
>(mPresShell
);
82 mSecondCaret
= MakeUnique
<AccessibleCaret
>(mPresShell
);
85 AccessibleCaretManager::~AccessibleCaretManager() {
86 MOZ_RELEASE_ASSERT(!mFlushingLayout
, "Going away in FlushLayout? Bad!");
89 void AccessibleCaretManager::Terminate() {
90 mFirstCaret
= nullptr;
91 mSecondCaret
= nullptr;
92 mActiveCaret
= nullptr;
96 nsresult
AccessibleCaretManager::OnSelectionChanged(Document
* aDoc
,
99 Selection
* selection
= GetSelection();
100 AC_LOG("%s: aSel: %p, GetSelection(): %p, aReason: %d", __FUNCTION__
, aSel
,
102 if (aSel
!= selection
) {
106 // eSetSelection events from the Fennec widget IME can be generated
107 // by autoSuggest / autoCorrect composition changes, or by TYPE_REPLACE_TEXT
108 // actions, either positioning cursor for text insert, or selecting
109 // text-to-be-replaced. None should affect AccessibleCaret visibility.
110 if (aReason
& nsISelectionListener::IME_REASON
) {
114 // Move the cursor by JavaScript or unknown internal call.
115 if (aReason
== nsISelectionListener::NO_REASON
) {
116 auto mode
= static_cast<ScriptUpdateMode
>(
117 StaticPrefs::layout_accessiblecaret_script_change_update_mode());
118 if (mode
== kScriptAlwaysShow
|| (mode
== kScriptUpdateVisible
&&
119 (mFirstCaret
->IsLogicallyVisible() ||
120 mSecondCaret
->IsLogicallyVisible()))) {
124 // Default for NO_REASON is to make hidden.
129 // Move cursor by keyboard.
130 if (aReason
& nsISelectionListener::KEYPRESS_REASON
) {
135 // OnBlur() might be called between mouse down and mouse up, so we hide carets
136 // upon mouse down anyway, and update carets upon mouse up.
137 if (aReason
& nsISelectionListener::MOUSEDOWN_REASON
) {
142 // Range will collapse after cutting or copying text.
143 if (aReason
& (nsISelectionListener::COLLAPSETOSTART_REASON
|
144 nsISelectionListener::COLLAPSETOEND_REASON
)) {
149 // For mouse input we don't want to show the carets.
150 if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
151 mLastInputSource
== MouseEvent_Binding::MOZ_SOURCE_MOUSE
) {
156 // When we want to hide the carets for mouse input, hide them for select
157 // all action fired by keyboard as well.
158 if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
159 mLastInputSource
== MouseEvent_Binding::MOZ_SOURCE_KEYBOARD
&&
160 (aReason
& nsISelectionListener::SELECTALL_REASON
)) {
169 void AccessibleCaretManager::HideCarets() {
170 if (mFirstCaret
->IsLogicallyVisible() || mSecondCaret
->IsLogicallyVisible()) {
171 AC_LOG("%s", __FUNCTION__
);
172 mFirstCaret
->SetAppearance(Appearance::None
);
173 mSecondCaret
->SetAppearance(Appearance::None
);
174 mIsCaretPositionChanged
= false;
175 DispatchCaretStateChangedEvent(CaretChangedReason::Visibilitychange
);
179 void AccessibleCaretManager::UpdateCarets(const UpdateCaretsHintSet
& aHint
) {
180 if (!FlushLayout()) {
184 mLastUpdateCaretMode
= GetCaretMode();
186 switch (mLastUpdateCaretMode
) {
187 case CaretMode::None
:
190 case CaretMode::Cursor
:
191 UpdateCaretsForCursorMode(aHint
);
193 case CaretMode::Selection
:
194 UpdateCaretsForSelectionMode(aHint
);
198 UpdateShouldDisableApz();
201 bool AccessibleCaretManager::IsCaretDisplayableInCursorMode(
202 nsIFrame
** aOutFrame
, int32_t* aOutOffset
) const {
203 RefPtr
<nsCaret
> caret
= mPresShell
->GetCaret();
204 if (!caret
|| !caret
->IsVisible()) {
210 nsCaret::GetFrameAndOffset(GetSelection(), nullptr, 0, &offset
);
216 if (!GetEditingHostForFrame(frame
)) {
225 *aOutOffset
= offset
;
231 bool AccessibleCaretManager::HasNonEmptyTextContent(nsINode
* aNode
) const {
232 return nsContentUtils::HasNonEmptyTextContent(
233 aNode
, nsContentUtils::eRecurseIntoChildren
);
236 void AccessibleCaretManager::UpdateCaretsForCursorMode(
237 const UpdateCaretsHintSet
& aHints
) {
238 AC_LOG("%s, selection: %p", __FUNCTION__
, GetSelection());
241 nsIFrame
* frame
= nullptr;
242 if (!IsCaretDisplayableInCursorMode(&frame
, &offset
)) {
247 PositionChangedResult result
= mFirstCaret
->SetPosition(frame
, offset
);
250 case PositionChangedResult::NotChanged
:
251 case PositionChangedResult::Position
:
252 case PositionChangedResult::Zoom
:
253 if (!aHints
.contains(UpdateCaretsHint::RespectOldAppearance
)) {
254 if (HasNonEmptyTextContent(GetEditingHostForFrame(frame
))) {
255 mFirstCaret
->SetAppearance(Appearance::Normal
);
258 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
259 if (mFirstCaret
->IsLogicallyVisible()) {
260 // Possible cases are: 1) SelectWordOrShortcut() sets the
261 // appearance to Normal. 2) When the caret is out of viewport and
262 // now scrolling into viewport, it has appearance NormalNotShown.
263 mFirstCaret
->SetAppearance(Appearance::Normal
);
265 // Possible cases are: a) Single tap on current empty content;
266 // OnSelectionChanged() sets the appearance to None due to
267 // MOUSEDOWN_REASON. b) Single tap on other empty content;
268 // OnBlur() sets the appearance to None.
270 // Do nothing to make the appearance remains None so that it can
271 // be distinguished from case 2). Also do not set the appearance
272 // to NormalNotShown here like the default update behavior.
275 mFirstCaret
->SetAppearance(Appearance::NormalNotShown
);
280 case PositionChangedResult::Invisible
:
281 mFirstCaret
->SetAppearance(Appearance::NormalNotShown
);
285 mSecondCaret
->SetAppearance(Appearance::None
);
287 mIsCaretPositionChanged
= (result
== PositionChangedResult::Position
);
289 if (!aHints
.contains(UpdateCaretsHint::DispatchNoEvent
) && !mActiveCaret
) {
290 DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition
);
294 void AccessibleCaretManager::UpdateCaretsForSelectionMode(
295 const UpdateCaretsHintSet
& aHints
) {
296 AC_LOG("%s: selection: %p", __FUNCTION__
, GetSelection());
298 int32_t startOffset
= 0;
299 nsIFrame
* startFrame
=
300 GetFrameForFirstRangeStartOrLastRangeEnd(eDirNext
, &startOffset
);
302 int32_t endOffset
= 0;
304 GetFrameForFirstRangeStartOrLastRangeEnd(eDirPrevious
, &endOffset
);
306 if (!CompareTreePosition(startFrame
, endFrame
)) {
307 // XXX: Do we really have to hide carets if this condition isn't satisfied?
312 auto updateSingleCaret
= [aHints
](AccessibleCaret
* aCaret
, nsIFrame
* aFrame
,
313 int32_t aOffset
) -> PositionChangedResult
{
314 PositionChangedResult result
= aCaret
->SetPosition(aFrame
, aOffset
);
317 case PositionChangedResult::NotChanged
:
318 case PositionChangedResult::Position
:
319 case PositionChangedResult::Zoom
:
320 if (!aHints
.contains(UpdateCaretsHint::RespectOldAppearance
)) {
321 aCaret
->SetAppearance(Appearance::Normal
);
325 case PositionChangedResult::Invisible
:
326 aCaret
->SetAppearance(Appearance::NormalNotShown
);
332 PositionChangedResult firstCaretResult
=
333 updateSingleCaret(mFirstCaret
.get(), startFrame
, startOffset
);
334 PositionChangedResult secondCaretResult
=
335 updateSingleCaret(mSecondCaret
.get(), endFrame
, endOffset
);
337 mIsCaretPositionChanged
=
338 firstCaretResult
== PositionChangedResult::Position
||
339 secondCaretResult
== PositionChangedResult::Position
;
341 if (mIsCaretPositionChanged
) {
342 // Flush layout to make the carets intersection correct.
343 if (!FlushLayout()) {
348 if (!aHints
.contains(UpdateCaretsHint::RespectOldAppearance
)) {
349 // Only check for tilt carets when the caller doesn't ask us to preserve
350 // old appearance. Otherwise we might override the appearance set by the
352 if (StaticPrefs::layout_accessiblecaret_always_tilt()) {
353 UpdateCaretsForAlwaysTilt(startFrame
, endFrame
);
355 UpdateCaretsForOverlappingTilt();
359 if (!aHints
.contains(UpdateCaretsHint::DispatchNoEvent
) && !mActiveCaret
) {
360 DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition
);
364 void AccessibleCaretManager::UpdateShouldDisableApz() {
366 // No need to disable APZ when dragging the caret.
367 mShouldDisableApz
= false;
371 if (mIsScrollStarted
) {
372 // During scrolling, the caret's position is changed only if it is in a
373 // position:fixed or a "stuck" position:sticky frame subtree.
374 mShouldDisableApz
= mIsCaretPositionChanged
;
378 // For other cases, we can only reliably detect whether the caret is in a
379 // position:fixed frame subtree.
380 switch (mLastUpdateCaretMode
) {
381 case CaretMode::None
:
382 mShouldDisableApz
= false;
384 case CaretMode::Cursor
:
385 mShouldDisableApz
= mFirstCaret
->IsVisuallyVisible() &&
386 mFirstCaret
->IsInPositionFixedSubtree();
388 case CaretMode::Selection
:
389 mShouldDisableApz
= (mFirstCaret
->IsVisuallyVisible() &&
390 mFirstCaret
->IsInPositionFixedSubtree()) ||
391 (mSecondCaret
->IsVisuallyVisible() &&
392 mSecondCaret
->IsInPositionFixedSubtree());
397 bool AccessibleCaretManager::UpdateCaretsForOverlappingTilt() {
398 if (!mFirstCaret
->IsVisuallyVisible() || !mSecondCaret
->IsVisuallyVisible()) {
402 if (!mFirstCaret
->Intersects(*mSecondCaret
)) {
403 mFirstCaret
->SetAppearance(Appearance::Normal
);
404 mSecondCaret
->SetAppearance(Appearance::Normal
);
408 if (mFirstCaret
->LogicalPosition().x
<= mSecondCaret
->LogicalPosition().x
) {
409 mFirstCaret
->SetAppearance(Appearance::Left
);
410 mSecondCaret
->SetAppearance(Appearance::Right
);
412 mFirstCaret
->SetAppearance(Appearance::Right
);
413 mSecondCaret
->SetAppearance(Appearance::Left
);
419 void AccessibleCaretManager::UpdateCaretsForAlwaysTilt(nsIFrame
* aStartFrame
,
420 nsIFrame
* aEndFrame
) {
421 // When a short LTR word in RTL environment is selected, the two carets
422 // tilted inward might be overlapped. Make them tilt outward.
423 if (UpdateCaretsForOverlappingTilt()) {
427 if (mFirstCaret
->IsVisuallyVisible()) {
428 auto startFrameWritingMode
= aStartFrame
->GetWritingMode();
429 mFirstCaret
->SetAppearance(startFrameWritingMode
.IsBidiLTR()
431 : Appearance::Right
);
433 if (mSecondCaret
->IsVisuallyVisible()) {
434 auto endFrameWritingMode
= aEndFrame
->GetWritingMode();
435 mSecondCaret
->SetAppearance(
436 endFrameWritingMode
.IsBidiLTR() ? Appearance::Right
: Appearance::Left
);
440 void AccessibleCaretManager::ProvideHapticFeedback() {
441 if (StaticPrefs::layout_accessiblecaret_hapticfeedback()) {
442 nsCOMPtr
<nsIHapticFeedback
> haptic
=
443 do_GetService("@mozilla.org/widget/hapticfeedback;1");
444 haptic
->PerformSimpleAction(haptic
->LongPress
);
448 nsresult
AccessibleCaretManager::PressCaret(const nsPoint
& aPoint
,
449 EventClassID aEventClass
) {
450 nsresult rv
= NS_ERROR_FAILURE
;
452 MOZ_ASSERT(aEventClass
== eMouseEventClass
|| aEventClass
== eTouchEventClass
,
453 "Unexpected event class!");
455 using TouchArea
= AccessibleCaret::TouchArea
;
456 TouchArea touchArea
=
457 aEventClass
== eMouseEventClass
? TouchArea::CaretImage
: TouchArea::Full
;
459 if (mFirstCaret
->Contains(aPoint
, touchArea
)) {
460 mActiveCaret
= mFirstCaret
.get();
461 SetSelectionDirection(eDirPrevious
);
462 } else if (mSecondCaret
->Contains(aPoint
, touchArea
)) {
463 mActiveCaret
= mSecondCaret
.get();
464 SetSelectionDirection(eDirNext
);
468 mOffsetYToCaretLogicalPosition
=
469 mActiveCaret
->LogicalPosition().y
- aPoint
.y
;
470 SetSelectionDragState(true);
471 DispatchCaretStateChangedEvent(CaretChangedReason::Presscaret
);
478 nsresult
AccessibleCaretManager::DragCaret(const nsPoint
& aPoint
) {
479 MOZ_ASSERT(mActiveCaret
);
480 MOZ_ASSERT(GetCaretMode() != CaretMode::None
);
482 if (!mPresShell
|| !mPresShell
->GetRootFrame() || !GetSelection()) {
483 return NS_ERROR_NULL_POINTER
;
486 StopSelectionAutoScrollTimer();
487 DragCaretInternal(aPoint
);
489 // We want to scroll the page even if we failed to drag the caret.
490 StartSelectionAutoScrollTimer(aPoint
);
495 nsresult
AccessibleCaretManager::ReleaseCaret() {
496 MOZ_ASSERT(mActiveCaret
);
498 mActiveCaret
= nullptr;
499 SetSelectionDragState(false);
500 UpdateShouldDisableApz();
501 DispatchCaretStateChangedEvent(CaretChangedReason::Releasecaret
);
505 nsresult
AccessibleCaretManager::TapCaret(const nsPoint
& aPoint
) {
506 MOZ_ASSERT(GetCaretMode() != CaretMode::None
);
508 nsresult rv
= NS_ERROR_FAILURE
;
510 if (GetCaretMode() == CaretMode::Cursor
) {
511 DispatchCaretStateChangedEvent(CaretChangedReason::Taponcaret
);
518 static EnumSet
<nsLayoutUtils::FrameForPointOption
> GetHitTestOptions() {
519 EnumSet
<nsLayoutUtils::FrameForPointOption
> options
= {
520 nsLayoutUtils::FrameForPointOption::IgnorePaintSuppression
,
521 nsLayoutUtils::FrameForPointOption::IgnoreCrossDoc
};
525 nsresult
AccessibleCaretManager::SelectWordOrShortcut(const nsPoint
& aPoint
) {
526 // If the long-tap is landing on a pre-existing selection, don't replace
527 // it with a new one. Instead just return and let the context menu pop up
528 // on the pre-existing selection.
529 if (GetCaretMode() == CaretMode::Selection
&&
530 GetSelection()->ContainsPoint(aPoint
)) {
531 AC_LOG("%s: UpdateCarets() for current selection", __FUNCTION__
);
533 ProvideHapticFeedback();
538 return NS_ERROR_UNEXPECTED
;
541 nsIFrame
* rootFrame
= mPresShell
->GetRootFrame();
543 return NS_ERROR_NOT_AVAILABLE
;
546 // Find the frame under point.
547 AutoWeakFrame ptFrame
= nsLayoutUtils::GetFrameForPoint(
548 RelativeTo
{rootFrame
}, aPoint
, GetHitTestOptions());
549 if (!ptFrame
.GetFrame()) {
550 return NS_ERROR_FAILURE
;
553 nsIFrame
* focusableFrame
= GetFocusableFrame(ptFrame
);
555 #ifdef DEBUG_FRAME_DUMP
556 AC_LOG("%s: Found %s under (%d, %d)", __FUNCTION__
, ptFrame
->ListTag().get(),
558 AC_LOG("%s: Found %s focusable", __FUNCTION__
,
559 focusableFrame
? focusableFrame
->ListTag().get() : "no frame");
562 // Get ptInFrame here so that we don't need to check whether rootFrame is
563 // alive later. Note that if ptFrame is being moved by
564 // IMEStateManager::NotifyIME() or ChangeFocusToOrClearOldFocus() below,
565 // something under the original point will be selected, which may not be the
566 // original text the user wants to select.
567 nsPoint ptInFrame
= aPoint
;
568 nsLayoutUtils::TransformPoint(RelativeTo
{rootFrame
}, RelativeTo
{ptFrame
},
571 // Firstly check long press on an empty editable content.
572 Element
* newFocusEditingHost
= GetEditingHostForFrame(ptFrame
);
573 if (focusableFrame
&& newFocusEditingHost
&&
574 !HasNonEmptyTextContent(newFocusEditingHost
)) {
575 ChangeFocusToOrClearOldFocus(focusableFrame
);
578 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
579 mFirstCaret
->SetAppearance(Appearance::Normal
);
581 // We need to update carets to get correct information before dispatching
582 // CaretStateChangedEvent.
584 ProvideHapticFeedback();
585 DispatchCaretStateChangedEvent(CaretChangedReason::Longpressonemptycontent
);
589 bool selectable
= ptFrame
->IsSelectable(nullptr);
591 #ifdef DEBUG_FRAME_DUMP
592 AC_LOG("%s: %s %s selectable.", __FUNCTION__
, ptFrame
->ListTag().get(),
593 selectable
? "is" : "is NOT");
597 return NS_ERROR_FAILURE
;
600 // Commit the composition string of the old editable focus element (if there
601 // is any) before changing the focus.
602 IMEStateManager::NotifyIME(widget::REQUEST_TO_COMMIT_COMPOSITION
,
603 mPresShell
->GetPresContext());
604 if (!ptFrame
.IsAlive()) {
605 // Cannot continue because ptFrame died.
606 return NS_ERROR_FAILURE
;
609 // ptFrame is selectable. Now change the focus.
610 ChangeFocusToOrClearOldFocus(focusableFrame
);
611 if (!ptFrame
.IsAlive()) {
612 // Cannot continue because ptFrame died.
613 return NS_ERROR_FAILURE
;
616 // If long tap point isn't selectable frame for caret and frame selection
617 // can find a better frame for caret, we don't select a word.
618 // See https://webcompat.com/issues/15953
619 nsIFrame::ContentOffsets offsets
=
620 ptFrame
->GetContentOffsetsFromPoint(ptInFrame
, nsIFrame::SKIP_HIDDEN
);
621 if (offsets
.content
) {
622 RefPtr
<nsFrameSelection
> frameSelection
= GetFrameSelection();
623 if (frameSelection
) {
625 nsIFrame
* theFrame
= nsFrameSelection::GetFrameForNodeOffset(
626 offsets
.content
, offsets
.offset
, offsets
.associate
, &offset
);
627 if (theFrame
&& theFrame
!= ptFrame
) {
628 SetSelectionDragState(true);
629 frameSelection
->HandleClick(
630 offsets
.content
, offsets
.StartOffset(), offsets
.EndOffset(),
631 nsFrameSelection::FocusMode::kCollapseToNewPoint
,
633 SetSelectionDragState(false);
634 ClearMaintainedSelection();
637 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
638 mFirstCaret
->SetAppearance(Appearance::Normal
);
642 ProvideHapticFeedback();
643 DispatchCaretStateChangedEvent(
644 CaretChangedReason::Longpressonemptycontent
);
651 // Then try select a word under point.
652 nsresult rv
= SelectWord(ptFrame
, ptInFrame
);
654 ProvideHapticFeedback();
659 void AccessibleCaretManager::OnScrollStart() {
660 AC_LOG("%s", __FUNCTION__
);
662 AutoRestore
<bool> saveAllowFlushingLayout(mAllowFlushingLayout
);
663 mAllowFlushingLayout
= false;
665 Maybe
<PresShell::AutoAssertNoFlush
> assert;
667 assert.emplace(*mPresShell
);
670 mIsScrollStarted
= true;
672 if (mFirstCaret
->IsLogicallyVisible() || mSecondCaret
->IsLogicallyVisible()) {
673 // Dispatch the event only if one of the carets is logically visible like in
675 DispatchCaretStateChangedEvent(CaretChangedReason::Scroll
);
679 void AccessibleCaretManager::OnScrollEnd() {
680 if (mLastUpdateCaretMode
!= GetCaretMode()) {
684 AutoRestore
<bool> saveAllowFlushingLayout(mAllowFlushingLayout
);
685 mAllowFlushingLayout
= false;
687 Maybe
<PresShell::AutoAssertNoFlush
> assert;
689 assert.emplace(*mPresShell
);
692 mIsScrollStarted
= false;
694 if (GetCaretMode() == CaretMode::Cursor
) {
695 if (!mFirstCaret
->IsLogicallyVisible()) {
696 // If the caret is hidden (Appearance::None) due to blur, no
697 // need to update it.
702 // For mouse input we don't want to show the carets.
703 if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
704 mLastInputSource
== MouseEvent_Binding::MOZ_SOURCE_MOUSE
) {
705 AC_LOG("%s: HideCarets()", __FUNCTION__
);
710 AC_LOG("%s: UpdateCarets()", __FUNCTION__
);
714 void AccessibleCaretManager::OnScrollPositionChanged() {
715 if (mLastUpdateCaretMode
!= GetCaretMode()) {
719 AutoRestore
<bool> saveAllowFlushingLayout(mAllowFlushingLayout
);
720 mAllowFlushingLayout
= false;
722 Maybe
<PresShell::AutoAssertNoFlush
> assert;
724 assert.emplace(*mPresShell
);
727 if (mFirstCaret
->IsLogicallyVisible() || mSecondCaret
->IsLogicallyVisible()) {
728 if (mIsScrollStarted
) {
729 // We don't want extra CaretStateChangedEvents dispatched when user is
730 // scrolling the page.
731 AC_LOG("%s: UpdateCarets(RespectOldAppearance | DispatchNoEvent)",
733 UpdateCarets({UpdateCaretsHint::RespectOldAppearance
,
734 UpdateCaretsHint::DispatchNoEvent
});
736 AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__
);
737 UpdateCarets(UpdateCaretsHint::RespectOldAppearance
);
742 void AccessibleCaretManager::OnReflow() {
743 if (mLastUpdateCaretMode
!= GetCaretMode()) {
747 AutoRestore
<bool> saveAllowFlushingLayout(mAllowFlushingLayout
);
748 mAllowFlushingLayout
= false;
750 Maybe
<PresShell::AutoAssertNoFlush
> assert;
752 assert.emplace(*mPresShell
);
755 if (mFirstCaret
->IsLogicallyVisible() || mSecondCaret
->IsLogicallyVisible()) {
756 AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__
);
757 UpdateCarets(UpdateCaretsHint::RespectOldAppearance
);
761 void AccessibleCaretManager::OnBlur() {
762 AC_LOG("%s: HideCarets()", __FUNCTION__
);
766 void AccessibleCaretManager::OnKeyboardEvent() {
767 if (GetCaretMode() == CaretMode::Cursor
) {
768 AC_LOG("%s: HideCarets()", __FUNCTION__
);
773 void AccessibleCaretManager::OnFrameReconstruction() {
774 mFirstCaret
->EnsureApzAware();
775 mSecondCaret
->EnsureApzAware();
778 void AccessibleCaretManager::SetLastInputSource(uint16_t aInputSource
) {
779 mLastInputSource
= aInputSource
;
782 Selection
* AccessibleCaretManager::GetSelection() const {
783 RefPtr
<nsFrameSelection
> fs
= GetFrameSelection();
787 return fs
->GetSelection(SelectionType::eNormal
);
790 already_AddRefed
<nsFrameSelection
> AccessibleCaretManager::GetFrameSelection()
796 nsFocusManager
* fm
= nsFocusManager::GetFocusManager();
799 nsIContent
* focusedContent
= fm
->GetFocusedElement();
800 if (!focusedContent
) {
801 // For non-editable content
802 return mPresShell
->FrameSelection();
805 nsIFrame
* focusFrame
= focusedContent
->GetPrimaryFrame();
810 // Prevent us from touching the nsFrameSelection associated with other
812 RefPtr
<nsFrameSelection
> fs
= focusFrame
->GetFrameSelection();
813 if (!fs
|| fs
->GetPresShell() != mPresShell
) {
820 nsAutoString
AccessibleCaretManager::StringifiedSelection() const {
822 RefPtr
<Selection
> selection
= GetSelection();
824 selection
->Stringify(str
, mAllowFlushingLayout
825 ? Selection::FlushFrames::Yes
826 : Selection::FlushFrames::No
);
831 Element
* AccessibleCaretManager::GetEditingHostForFrame(
832 nsIFrame
* aFrame
) const {
837 auto content
= aFrame
->GetContent();
842 return content
->GetEditingHost();
845 AccessibleCaretManager::CaretMode
AccessibleCaretManager::GetCaretMode() const {
846 Selection
* selection
= GetSelection();
848 return CaretMode::None
;
851 uint32_t rangeCount
= selection
->RangeCount();
852 if (rangeCount
<= 0) {
853 return CaretMode::None
;
856 nsFocusManager
* fm
= nsFocusManager::GetFocusManager();
858 if (fm
->GetFocusedWindow() != mPresShell
->GetDocument()->GetWindow()) {
859 // Hide carets if the window is not focused.
860 return CaretMode::None
;
863 if (selection
->IsCollapsed()) {
864 return CaretMode::Cursor
;
867 return CaretMode::Selection
;
870 nsIFrame
* AccessibleCaretManager::GetFocusableFrame(nsIFrame
* aFrame
) const {
871 // This implementation is similar to EventStateManager::PostHandleEvent().
872 // Look for the nearest enclosing focusable frame.
873 nsIFrame
* focusableFrame
= aFrame
;
874 while (focusableFrame
) {
875 if (focusableFrame
->IsFocusable(nullptr, true)) {
878 focusableFrame
= focusableFrame
->GetParent();
880 return focusableFrame
;
883 void AccessibleCaretManager::ChangeFocusToOrClearOldFocus(
884 nsIFrame
* aFrame
) const {
885 nsFocusManager
* fm
= nsFocusManager::GetFocusManager();
889 nsIContent
* focusableContent
= aFrame
->GetContent();
890 MOZ_ASSERT(focusableContent
, "Focusable frame must have content!");
891 RefPtr
<Element
> focusableElement
= Element::FromNode(focusableContent
);
892 fm
->SetFocus(focusableElement
, nsIFocusManager::FLAG_BYLONGPRESS
);
894 nsPIDOMWindowOuter
* win
= mPresShell
->GetDocument()->GetWindow();
897 fm
->SetFocusedWindow(win
);
902 nsresult
AccessibleCaretManager::SelectWord(nsIFrame
* aFrame
,
903 const nsPoint
& aPoint
) const {
904 SetSelectionDragState(true);
905 nsresult rs
= aFrame
->SelectByTypeAtPoint(
906 mPresShell
->GetPresContext(), aPoint
, eSelectWord
, eSelectWord
, 0);
908 SetSelectionDragState(false);
909 ClearMaintainedSelection();
911 // Smart-select phone numbers if possible.
912 if (StaticPrefs::layout_accessiblecaret_extend_selection_for_phone_number()) {
913 SelectMoreIfPhoneNumber();
919 void AccessibleCaretManager::SetSelectionDragState(bool aState
) const {
920 RefPtr
<nsFrameSelection
> fs
= GetFrameSelection();
922 fs
->SetDragState(aState
);
926 bool AccessibleCaretManager::IsPhoneNumber(nsAString
& aCandidate
) const {
927 RefPtr
<Document
> doc
= mPresShell
->GetDocument();
928 nsAutoString
phoneNumberRegex(u
"(^\\+)?[0-9 ,\\-.()*#pw]{1,30}$"_ns
);
929 return nsContentUtils::IsPatternMatching(aCandidate
, phoneNumberRegex
, doc
)
933 void AccessibleCaretManager::SelectMoreIfPhoneNumber() const {
934 nsAutoString selectedText
= StringifiedSelection();
936 if (IsPhoneNumber(selectedText
)) {
937 SetSelectionDirection(eDirNext
);
938 ExtendPhoneNumberSelection(u
"forward"_ns
);
940 SetSelectionDirection(eDirPrevious
);
941 ExtendPhoneNumberSelection(u
"backward"_ns
);
943 SetSelectionDirection(eDirNext
);
947 void AccessibleCaretManager::ExtendPhoneNumberSelection(
948 const nsAString
& aDirection
) const {
953 // Extend the phone number selection until we find a boundary.
954 RefPtr
<Selection
> selection
= GetSelection();
957 const nsRange
* anchorFocusRange
= selection
->GetAnchorFocusRange();
958 if (!anchorFocusRange
) {
962 // Backup the anchor focus range since both anchor node and focus node might
963 // be changed after calling Selection::Modify().
964 RefPtr
<nsRange
> oldAnchorFocusRange
= anchorFocusRange
->CloneRange();
966 // Save current focus node, focus offset and the selected text so that
967 // we can compare them with the modified ones later.
968 nsINode
* oldFocusNode
= selection
->GetFocusNode();
969 uint32_t oldFocusOffset
= selection
->FocusOffset();
970 nsAutoString oldSelectedText
= StringifiedSelection();
972 // Extend the selection by one char.
973 selection
->Modify(u
"extend"_ns
, aDirection
, u
"character"_ns
,
975 if (IsTerminated()) {
979 // If the selection didn't change, (can't extend further), we're done.
980 if (selection
->GetFocusNode() == oldFocusNode
&&
981 selection
->FocusOffset() == oldFocusOffset
) {
985 // If the changed selection isn't a valid phone number, we're done.
986 // Also, if the selection was extended to a new block node, the string
987 // returned by stringify() won't have a new line at the beginning or the
988 // end of the string. Therefore, if either focus node or offset is
989 // changed, but selected text is not changed, we're done, too.
990 nsAutoString selectedText
= StringifiedSelection();
992 if (!IsPhoneNumber(selectedText
) || oldSelectedText
== selectedText
) {
993 // Backout the undesired selection extend, restore the old anchor focus
994 // range before exit.
995 selection
->SetAnchorFocusToRange(oldAnchorFocusRange
);
1001 void AccessibleCaretManager::SetSelectionDirection(nsDirection aDir
) const {
1002 Selection
* selection
= GetSelection();
1004 selection
->AdjustAnchorFocusForMultiRange(aDir
);
1008 void AccessibleCaretManager::ClearMaintainedSelection() const {
1009 // Selection made by double-clicking for example will maintain the original
1010 // word selection. We should clear it so that we can drag caret freely.
1011 RefPtr
<nsFrameSelection
> fs
= GetFrameSelection();
1013 fs
->MaintainSelection(eSelectNoAmount
);
1017 bool AccessibleCaretManager::FlushLayout() {
1018 if (mPresShell
&& mAllowFlushingLayout
) {
1019 AutoRestore
<bool> flushing(mFlushingLayout
);
1020 mFlushingLayout
= true;
1022 if (Document
* doc
= mPresShell
->GetDocument()) {
1023 doc
->FlushPendingNotifications(FlushType::Layout
);
1027 return !IsTerminated();
1030 nsIFrame
* AccessibleCaretManager::GetFrameForFirstRangeStartOrLastRangeEnd(
1031 nsDirection aDirection
, int32_t* aOutOffset
, nsIContent
** aOutContent
,
1032 int32_t* aOutContentOffset
) const {
1037 MOZ_ASSERT(GetCaretMode() == CaretMode::Selection
);
1038 MOZ_ASSERT(aOutOffset
, "aOutOffset shouldn't be nullptr!");
1040 const nsRange
* range
= nullptr;
1041 RefPtr
<nsINode
> startNode
;
1042 RefPtr
<nsINode
> endNode
;
1043 int32_t nodeOffset
= 0;
1044 CaretAssociationHint hint
;
1046 RefPtr
<Selection
> selection
= GetSelection();
1047 bool findInFirstRangeStart
= aDirection
== eDirNext
;
1049 if (findInFirstRangeStart
) {
1050 range
= selection
->GetRangeAt(0);
1051 startNode
= range
->GetStartContainer();
1052 endNode
= range
->GetEndContainer();
1053 nodeOffset
= range
->StartOffset();
1054 hint
= CARET_ASSOCIATE_AFTER
;
1056 range
= selection
->GetRangeAt(selection
->RangeCount() - 1);
1057 startNode
= range
->GetEndContainer();
1058 endNode
= range
->GetStartContainer();
1059 nodeOffset
= range
->EndOffset();
1060 hint
= CARET_ASSOCIATE_BEFORE
;
1063 nsCOMPtr
<nsIContent
> startContent
= do_QueryInterface(startNode
);
1064 nsIFrame
* startFrame
= nsFrameSelection::GetFrameForNodeOffset(
1065 startContent
, nodeOffset
, hint
, aOutOffset
);
1069 RefPtr
<TreeWalker
> walker
= mPresShell
->GetDocument()->CreateTreeWalker(
1070 *startNode
, dom::NodeFilter_Binding::SHOW_ALL
, nullptr, err
);
1076 startFrame
= startContent
? startContent
->GetPrimaryFrame() : nullptr;
1077 while (!startFrame
&& startNode
!= endNode
) {
1078 startNode
= findInFirstRangeStart
? walker
->NextNode(err
)
1079 : walker
->PreviousNode(err
);
1085 startContent
= startNode
->AsContent();
1086 startFrame
= startContent
? startContent
->GetPrimaryFrame() : nullptr;
1089 // We are walking among the nodes in the content tree, so the node offset
1090 // relative to startNode should be set to 0.
1097 startContent
.forget(aOutContent
);
1099 if (aOutContentOffset
) {
1100 *aOutContentOffset
= nodeOffset
;
1107 bool AccessibleCaretManager::RestrictCaretDraggingOffsets(
1108 nsIFrame::ContentOffsets
& aOffsets
) {
1113 MOZ_ASSERT(GetCaretMode() == CaretMode::Selection
);
1115 nsDirection dir
= mActiveCaret
== mFirstCaret
.get() ? eDirPrevious
: eDirNext
;
1117 nsCOMPtr
<nsIContent
> content
;
1118 int32_t contentOffset
= 0;
1119 nsIFrame
* frame
= GetFrameForFirstRangeStartOrLastRangeEnd(
1120 dir
, &offset
, getter_AddRefs(content
), &contentOffset
);
1126 // Compare the active caret's new position (aOffsets) to the inactive caret's
1128 const Maybe
<int32_t> cmpToInactiveCaretPos
= nsContentUtils::ComparePoints(
1129 aOffsets
.content
, aOffsets
.StartOffset(), content
, contentOffset
);
1130 if (NS_WARN_IF(!cmpToInactiveCaretPos
)) {
1131 // Potentially handle this properly when Selection across Shadow DOM
1132 // boundary is implemented
1133 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1137 // Move one character (in the direction of dir) from the inactive caret's
1138 // position. This is the limit for the active caret's new position.
1139 nsPeekOffsetStruct
limit(eSelectCluster
, dir
, offset
, nsPoint(0, 0), true,
1140 true, false, false, false);
1141 nsresult rv
= frame
->PeekOffset(&limit
);
1142 if (NS_FAILED(rv
)) {
1143 limit
.mResultContent
= content
;
1144 limit
.mContentOffset
= contentOffset
;
1147 // Compare the active caret's new position (aOffsets) to the limit.
1148 const Maybe
<int32_t> cmpToLimit
=
1149 nsContentUtils::ComparePoints(aOffsets
.content
, aOffsets
.StartOffset(),
1150 limit
.mResultContent
, limit
.mContentOffset
);
1151 if (NS_WARN_IF(!cmpToLimit
)) {
1152 // Potentially handle this properly when Selection across Shadow DOM
1153 // boundary is implemented
1154 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1158 auto SetOffsetsToLimit
= [&aOffsets
, &limit
]() {
1159 aOffsets
.content
= limit
.mResultContent
;
1160 aOffsets
.offset
= limit
.mContentOffset
;
1161 aOffsets
.secondaryOffset
= limit
.mContentOffset
;
1165 layout_accessiblecaret_allow_dragging_across_other_caret()) {
1166 if ((mActiveCaret
== mFirstCaret
.get() && *cmpToLimit
== 1) ||
1167 (mActiveCaret
== mSecondCaret
.get() && *cmpToLimit
== -1)) {
1168 // The active caret's position is past the limit, which we don't allow
1169 // here. So set it to the limit, resulting in one character being
1171 SetOffsetsToLimit();
1174 switch (*cmpToInactiveCaretPos
) {
1176 // The active caret's position is the same as the position of the
1177 // inactive caret. So set it to the limit to prevent the selection from
1178 // being collapsed, resulting in one character being selected.
1179 SetOffsetsToLimit();
1182 if (mActiveCaret
== mFirstCaret
.get()) {
1183 // First caret was moved across the second caret. After making change
1184 // to the selection, the user will drag the second caret.
1185 mActiveCaret
= mSecondCaret
.get();
1189 if (mActiveCaret
== mSecondCaret
.get()) {
1190 // Second caret was moved across the first caret. After making change
1191 // to the selection, the user will drag the first caret.
1192 mActiveCaret
= mFirstCaret
.get();
1201 bool AccessibleCaretManager::CompareTreePosition(nsIFrame
* aStartFrame
,
1202 nsIFrame
* aEndFrame
) const {
1203 return (aStartFrame
&& aEndFrame
&&
1204 nsLayoutUtils::CompareTreePosition(aStartFrame
, aEndFrame
) <= 0);
1207 nsresult
AccessibleCaretManager::DragCaretInternal(const nsPoint
& aPoint
) {
1208 MOZ_ASSERT(mPresShell
);
1210 nsIFrame
* rootFrame
= mPresShell
->GetRootFrame();
1211 MOZ_ASSERT(rootFrame
, "We need root frame to compute caret dragging!");
1213 nsPoint point
= AdjustDragBoundary(
1214 nsPoint(aPoint
.x
, aPoint
.y
+ mOffsetYToCaretLogicalPosition
));
1216 // Find out which content we point to
1218 nsIFrame
* ptFrame
= nsLayoutUtils::GetFrameForPoint(
1219 RelativeTo
{rootFrame
}, point
, GetHitTestOptions());
1221 return NS_ERROR_FAILURE
;
1224 RefPtr
<nsFrameSelection
> fs
= GetFrameSelection();
1228 nsIFrame
* newFrame
= nullptr;
1230 nsPoint ptInFrame
= point
;
1231 nsLayoutUtils::TransformPoint(RelativeTo
{rootFrame
}, RelativeTo
{ptFrame
},
1233 result
= fs
->ConstrainFrameAndPointToAnchorSubtree(ptFrame
, ptInFrame
,
1234 &newFrame
, newPoint
);
1235 if (NS_FAILED(result
) || !newFrame
) {
1236 return NS_ERROR_FAILURE
;
1239 if (!newFrame
->IsSelectable(nullptr)) {
1240 return NS_ERROR_FAILURE
;
1243 nsIFrame::ContentOffsets offsets
=
1244 newFrame
->GetContentOffsetsFromPoint(newPoint
);
1245 if (offsets
.IsNull()) {
1246 return NS_ERROR_FAILURE
;
1249 if (GetCaretMode() == CaretMode::Selection
&&
1250 !RestrictCaretDraggingOffsets(offsets
)) {
1251 return NS_ERROR_FAILURE
;
1254 ClearMaintainedSelection();
1256 const nsFrameSelection::FocusMode focusMode
=
1257 (GetCaretMode() == CaretMode::Selection
)
1258 ? nsFrameSelection::FocusMode::kExtendSelection
1259 : nsFrameSelection::FocusMode::kCollapseToNewPoint
;
1260 fs
->HandleClick(offsets
.content
, offsets
.StartOffset(), offsets
.EndOffset(),
1261 focusMode
, offsets
.associate
);
1265 nsRect
AccessibleCaretManager::GetAllChildFrameRectsUnion(
1266 nsIFrame
* aFrame
) const {
1269 // Drill through scroll frames, we don't want to include scrollbar child
1271 for (nsIFrame
* frame
= aFrame
->GetContentInsertionFrame(); frame
;
1272 frame
= frame
->GetNextContinuation()) {
1275 for (const auto& childList
: frame
->ChildLists()) {
1276 // Loop all children to union their scrollable overflow rect.
1277 for (nsIFrame
* child
: childList
.mList
) {
1278 nsRect childRect
= child
->GetScrollableOverflowRectRelativeToSelf();
1279 nsLayoutUtils::TransformRect(child
, frame
, childRect
);
1281 // A TextFrame containing only '\n' has positive height and width 0, or
1282 // positive width and height 0 if it's vertical. Need to use UnionEdges
1283 // to add its rect. BRFrame rect should be non-empty.
1284 if (childRect
.IsEmpty()) {
1285 frameRect
= frameRect
.UnionEdges(childRect
);
1287 frameRect
= frameRect
.Union(childRect
);
1292 MOZ_ASSERT(!frameRect
.IsEmpty(),
1293 "Editable frames should have at least one BRFrame child to make "
1294 "frameRect non-empty!");
1295 if (frame
!= aFrame
) {
1296 nsLayoutUtils::TransformRect(frame
, aFrame
, frameRect
);
1298 unionRect
= unionRect
.Union(frameRect
);
1304 nsPoint
AccessibleCaretManager::AdjustDragBoundary(
1305 const nsPoint
& aPoint
) const {
1306 nsPoint adjustedPoint
= aPoint
;
1308 int32_t focusOffset
= 0;
1309 nsIFrame
* focusFrame
=
1310 nsCaret::GetFrameAndOffset(GetSelection(), nullptr, 0, &focusOffset
);
1311 Element
* editingHost
= GetEditingHostForFrame(focusFrame
);
1314 nsIFrame
* editingHostFrame
= editingHost
->GetPrimaryFrame();
1315 if (editingHostFrame
) {
1316 nsRect boundary
= GetAllChildFrameRectsUnion(editingHostFrame
);
1317 nsLayoutUtils::TransformRect(editingHostFrame
, mPresShell
->GetRootFrame(),
1320 // Shrink the rect to make sure we never hit the boundary.
1321 boundary
.Deflate(kBoundaryAppUnits
);
1323 adjustedPoint
= boundary
.ClampPoint(adjustedPoint
);
1327 if (GetCaretMode() == CaretMode::Selection
&&
1329 layout_accessiblecaret_allow_dragging_across_other_caret()) {
1330 // Bug 1068474: Adjust the Y-coordinate so that the carets won't be in tilt
1331 // mode when a caret is being dragged surpass the other caret.
1333 // For example, when dragging the second caret, the horizontal boundary
1334 // (lower bound) of its Y-coordinate is the logical position of the first
1335 // caret. Likewise, when dragging the first caret, the horizontal boundary
1336 // (upper bound) of its Y-coordinate is the logical position of the second
1338 if (mActiveCaret
== mFirstCaret
.get()) {
1339 nscoord dragDownBoundaryY
= mSecondCaret
->LogicalPosition().y
;
1340 if (dragDownBoundaryY
> 0 && adjustedPoint
.y
> dragDownBoundaryY
) {
1341 adjustedPoint
.y
= dragDownBoundaryY
;
1344 nscoord dragUpBoundaryY
= mFirstCaret
->LogicalPosition().y
;
1345 if (adjustedPoint
.y
< dragUpBoundaryY
) {
1346 adjustedPoint
.y
= dragUpBoundaryY
;
1351 return adjustedPoint
;
1354 void AccessibleCaretManager::StartSelectionAutoScrollTimer(
1355 const nsPoint
& aPoint
) const {
1356 Selection
* selection
= GetSelection();
1357 MOZ_ASSERT(selection
);
1359 nsIFrame
* anchorFrame
= selection
->GetPrimaryFrameForAnchorNode();
1364 nsIScrollableFrame
* scrollFrame
= nsLayoutUtils::GetNearestScrollableFrame(
1365 anchorFrame
, nsLayoutUtils::SCROLLABLE_SAME_DOC
|
1366 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN
);
1371 nsIFrame
* capturingFrame
= scrollFrame
->GetScrolledFrame();
1372 if (!capturingFrame
) {
1376 nsIFrame
* rootFrame
= mPresShell
->GetRootFrame();
1377 MOZ_ASSERT(rootFrame
);
1378 nsPoint ptInScrolled
= aPoint
;
1379 nsLayoutUtils::TransformPoint(RelativeTo
{rootFrame
},
1380 RelativeTo
{capturingFrame
}, ptInScrolled
);
1382 RefPtr
<nsFrameSelection
> fs
= GetFrameSelection();
1384 fs
->StartAutoScrollTimer(capturingFrame
, ptInScrolled
, kAutoScrollTimerDelay
);
1387 void AccessibleCaretManager::StopSelectionAutoScrollTimer() const {
1388 RefPtr
<nsFrameSelection
> fs
= GetFrameSelection();
1390 fs
->StopAutoScrollTimer();
1393 void AccessibleCaretManager::DispatchCaretStateChangedEvent(
1394 CaretChangedReason aReason
) {
1395 if (!FlushLayout()) {
1399 Selection
* sel
= GetSelection();
1404 Document
* doc
= mPresShell
->GetDocument();
1407 CaretStateChangedEventInit init
;
1408 init
.mBubbles
= true;
1410 const nsRange
* range
= sel
->GetAnchorFocusRange();
1411 nsINode
* commonAncestorNode
= nullptr;
1413 commonAncestorNode
= range
->GetClosestCommonInclusiveAncestor();
1416 if (!commonAncestorNode
) {
1417 commonAncestorNode
= sel
->GetFrameSelection()->GetAncestorLimiter();
1420 RefPtr
<DOMRect
> domRect
= new DOMRect(ToSupports(doc
));
1421 nsRect rect
= nsLayoutUtils::GetSelectionBoundingRect(sel
);
1423 nsIFrame
* commonAncestorFrame
= nullptr;
1424 nsIFrame
* rootFrame
= mPresShell
->GetRootFrame();
1426 if (commonAncestorNode
&& commonAncestorNode
->IsContent()) {
1427 commonAncestorFrame
= commonAncestorNode
->AsContent()->GetPrimaryFrame();
1430 if (commonAncestorFrame
&& rootFrame
) {
1431 nsLayoutUtils::TransformRect(rootFrame
, commonAncestorFrame
, rect
);
1432 nsRect clampedRect
=
1433 nsLayoutUtils::ClampRectToScrollFrames(commonAncestorFrame
, rect
);
1434 nsLayoutUtils::TransformRect(commonAncestorFrame
, rootFrame
, clampedRect
);
1436 init
.mSelectionVisible
= !clampedRect
.IsEmpty();
1438 init
.mSelectionVisible
= true;
1441 // The rect computed above is relative to rootFrame, which is the (layout)
1442 // viewport frame. However, the consumers of this event expect the bounds
1443 // of the selection relative to the screen (visual viewport origin), so
1444 // translate between the two.
1445 rect
-= mPresShell
->GetVisualViewportOffsetRelativeToLayoutViewport();
1447 domRect
->SetLayoutRect(rect
);
1449 // Send isEditable info w/ event detail. This info can help determine
1450 // whether to show cut command on selection dialog or not.
1451 init
.mSelectionEditable
=
1452 commonAncestorFrame
&& GetEditingHostForFrame(commonAncestorFrame
);
1454 init
.mBoundingClientRect
= domRect
;
1455 init
.mReason
= aReason
;
1456 init
.mCollapsed
= sel
->IsCollapsed();
1457 init
.mCaretVisible
=
1458 mFirstCaret
->IsLogicallyVisible() || mSecondCaret
->IsLogicallyVisible();
1459 init
.mCaretVisuallyVisible
=
1460 mFirstCaret
->IsVisuallyVisible() || mSecondCaret
->IsVisuallyVisible();
1461 init
.mSelectedTextContent
= StringifiedSelection();
1463 RefPtr
<CaretStateChangedEvent
> event
= CaretStateChangedEvent::Constructor(
1464 doc
, u
"mozcaretstatechanged"_ns
, init
);
1466 event
->SetTrusted(true);
1467 event
->WidgetEventPtr()->mFlags
.mOnlyChromeDispatch
= true;
1469 AC_LOG("%s: reason %" PRIu32
", collapsed %d, caretVisible %" PRIu32
,
1470 __FUNCTION__
, static_cast<uint32_t>(init
.mReason
), init
.mCollapsed
,
1471 static_cast<uint32_t>(init
.mCaretVisible
));
1473 (new AsyncEventDispatcher(doc
, event
))->PostDOMEvent();
1476 } // namespace mozilla