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/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"
29 #include "nsContainerFrame.h"
30 #include "nsContentUtils.h"
32 #include "nsFocusManager.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"
44 #define AC_LOG(message, ...) \
45 AC_LOG_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
48 #define AC_LOGV(message, ...) \
49 AC_LOGV_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
52 using Appearance
= AccessibleCaret::Appearance
;
53 using PositionChangedResult
= AccessibleCaret::PositionChangedResult
;
55 #define AC_PROCESS_ENUM_TO_STREAM(e) \
59 std::ostream
& operator<<(std::ostream
& aStream
,
60 const AccessibleCaretManager::CaretMode
& aCaretMode
) {
61 using CaretMode
= AccessibleCaretManager::CaretMode
;
63 AC_PROCESS_ENUM_TO_STREAM(CaretMode::None
);
64 AC_PROCESS_ENUM_TO_STREAM(CaretMode::Cursor
);
65 AC_PROCESS_ENUM_TO_STREAM(CaretMode::Selection
);
70 std::ostream
& operator<<(
71 std::ostream
& aStream
,
72 const AccessibleCaretManager::UpdateCaretsHint
& aHint
) {
73 using UpdateCaretsHint
= AccessibleCaretManager::UpdateCaretsHint
;
75 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::Default
);
76 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::RespectOldAppearance
);
77 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::DispatchNoEvent
);
81 #undef AC_PROCESS_ENUM_TO_STREAM
83 AccessibleCaretManager::AccessibleCaretManager(PresShell
* aPresShell
)
84 : AccessibleCaretManager
{
86 Carets
{aPresShell
? MakeUnique
<AccessibleCaret
>(aPresShell
) : nullptr,
87 aPresShell
? MakeUnique
<AccessibleCaret
>(aPresShell
)
90 AccessibleCaretManager::AccessibleCaretManager(PresShell
* aPresShell
,
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() {
100 mActiveCaret
= nullptr;
101 mPresShell
= nullptr;
104 nsresult
AccessibleCaretManager::OnSelectionChanged(Document
* aDoc
,
107 Selection
* selection
= GetSelection();
108 AC_LOG("%s: aSel: %p, GetSelection(): %p, aReason: %d", __FUNCTION__
, aSel
,
110 if (aSel
!= selection
) {
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
) {
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())) {
132 // Default for NO_REASON is to make hidden.
133 HideCaretsAndDispatchCaretStateChangedEvent();
137 // Move cursor by keyboard.
138 if (aReason
& nsISelectionListener::KEYPRESS_REASON
) {
139 HideCaretsAndDispatchCaretStateChangedEvent();
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();
150 // Range will collapse after cutting or copying text.
151 if (aReason
& (nsISelectionListener::COLLAPSETOSTART_REASON
|
152 nsISelectionListener::COLLAPSETOEND_REASON
)) {
153 HideCaretsAndDispatchCaretStateChangedEvent();
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();
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();
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
{
189 // `MaybeFlush` doesn't access the PresShell after flushing, so it's OK to
191 mLayoutFlusher
.MaybeFlush(MOZ_KnownLive(*mPresShell
));
194 return IsTerminated();
197 void AccessibleCaretManager::UpdateCarets(const UpdateCaretsHintSet
& aHint
) {
198 if (MaybeFlushLayout() == Terminated::Yes
) {
202 mLastUpdateCaretMode
= GetCaretMode();
204 switch (mLastUpdateCaretMode
) {
205 case CaretMode::None
:
206 HideCaretsAndDispatchCaretStateChangedEvent();
208 case CaretMode::Cursor
:
209 UpdateCaretsForCursorMode(aHint
);
211 case CaretMode::Selection
:
212 UpdateCaretsForSelectionMode(aHint
);
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()) {
228 nsCaret::GetFrameAndOffset(GetSelection(), nullptr, 0, &offset
);
234 if (!GetEditingHostForFrame(frame
)) {
243 *aOutOffset
= offset
;
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());
259 nsIFrame
* frame
= nullptr;
260 if (!IsCaretDisplayableInCursorMode(&frame
, &offset
)) {
261 HideCaretsAndDispatchCaretStateChangedEvent();
265 PositionChangedResult result
= mCarets
.GetFirst()->SetPosition(frame
, offset
);
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
);
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
);
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.
293 mCarets
.GetFirst()->SetAppearance(Appearance::NormalNotShown
);
298 case PositionChangedResult::Invisible
:
299 mCarets
.GetFirst()->SetAppearance(Appearance::NormalNotShown
);
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;
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();
330 auto updateSingleCaret
= [aHints
](AccessibleCaret
* aCaret
, nsIFrame
* aFrame
,
331 int32_t aOffset
) -> PositionChangedResult
{
332 PositionChangedResult result
= aCaret
->SetPosition(aFrame
, aOffset
);
335 case PositionChangedResult::NotChanged
:
336 case PositionChangedResult::Position
:
337 case PositionChangedResult::Zoom
:
338 if (!aHints
.contains(UpdateCaretsHint::RespectOldAppearance
)) {
339 aCaret
->SetAppearance(Appearance::Normal
);
343 case PositionChangedResult::Invisible
:
344 aCaret
->SetAppearance(Appearance::NormalNotShown
);
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
) {
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
370 if (StaticPrefs::layout_accessiblecaret_always_tilt()) {
371 UpdateCaretsForAlwaysTilt(startFrame
, endFrame
);
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
;
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
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
;
404 case CaretMode::Cursor
:
406 (aAccessibleCaretManager
.mCarets
.GetFirst()->IsVisuallyVisible() &&
407 aAccessibleCaretManager
.mCarets
.GetFirst()
408 ->IsInPositionFixedSubtree())
412 case CaretMode::Selection
:
414 ((aAccessibleCaretManager
.mCarets
.GetFirst()->IsVisuallyVisible() &&
415 aAccessibleCaretManager
.mCarets
.GetFirst()
416 ->IsInPositionFixedSubtree()) ||
417 (aAccessibleCaretManager
.mCarets
.GetSecond()->IsVisuallyVisible() &&
418 aAccessibleCaretManager
.mCarets
.GetSecond()
419 ->IsInPositionFixedSubtree()))
426 bool AccessibleCaretManager::UpdateCaretsForOverlappingTilt() {
427 if (!mCarets
.GetFirst()->IsVisuallyVisible() ||
428 !mCarets
.GetSecond()->IsVisuallyVisible()) {
432 if (!mCarets
.GetFirst()->Intersects(*mCarets
.GetSecond())) {
433 mCarets
.GetFirst()->SetAppearance(Appearance::Normal
);
434 mCarets
.GetSecond()->SetAppearance(Appearance::Normal
);
438 if (mCarets
.GetFirst()->LogicalPosition().x
<=
439 mCarets
.GetSecond()->LogicalPosition().x
) {
440 mCarets
.GetFirst()->SetAppearance(Appearance::Left
);
441 mCarets
.GetSecond()->SetAppearance(Appearance::Right
);
443 mCarets
.GetFirst()->SetAppearance(Appearance::Right
);
444 mCarets
.GetSecond()->SetAppearance(Appearance::Left
);
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()) {
458 if (mCarets
.GetFirst()->IsVisuallyVisible()) {
459 auto startFrameWritingMode
= aStartFrame
->GetWritingMode();
460 mCarets
.GetFirst()->SetAppearance(startFrameWritingMode
.IsBidiLTR()
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
);
500 mOffsetYToCaretLogicalPosition
=
501 mActiveCaret
->LogicalPosition().y
- aPoint
.y
;
502 SetSelectionDragState(true);
503 DispatchCaretStateChangedEvent(CaretChangedReason::Presscaret
, &aPoint
);
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
);
525 if (StaticPrefs::layout_accessiblecaret_magnifier_enabled()) {
526 DispatchCaretStateChangedEvent(CaretChangedReason::Dragcaret
, &aPoint
);
531 nsresult
AccessibleCaretManager::ReleaseCaret() {
532 MOZ_ASSERT(mActiveCaret
);
534 mActiveCaret
= nullptr;
535 SetSelectionDragState(false);
536 mDesiredAsyncPanZoomState
.Update(*this);
537 DispatchCaretStateChangedEvent(CaretChangedReason::Releasecaret
);
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
);
554 static EnumSet
<nsLayoutUtils::FrameForPointOption
> GetHitTestOptions() {
555 EnumSet
<nsLayoutUtils::FrameForPointOption
> options
= {
556 nsLayoutUtils::FrameForPointOption::IgnorePaintSuppression
,
557 nsLayoutUtils::FrameForPointOption::IgnoreCrossDoc
};
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__
);
569 ProvideHapticFeedback();
574 return NS_ERROR_UNEXPECTED
;
577 nsIFrame
* rootFrame
= mPresShell
->GetRootFrame();
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(),
594 AC_LOG("%s: Found %s focusable", __FUNCTION__
,
595 focusableFrame
? focusableFrame
->ListTag().get() : "no frame");
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
},
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
);
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.
620 ProvideHapticFeedback();
621 DispatchCaretStateChangedEvent(CaretChangedReason::Longpressonemptycontent
);
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");
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(
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
,
670 SetSelectionDragState(false);
671 ClearMaintainedSelection();
674 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
675 mCarets
.GetFirst()->SetAppearance(Appearance::Normal
);
679 ProvideHapticFeedback();
680 DispatchCaretStateChangedEvent(
681 CaretChangedReason::Longpressonemptycontent
);
688 // Then try select a word under point.
689 nsresult rv
= SelectWord(ptFrame
, ptInFrame
);
691 ProvideHapticFeedback();
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;
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;
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.
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();
746 AC_LOG("%s: UpdateCarets()", __FUNCTION__
);
750 void AccessibleCaretManager::OnScrollPositionChanged() {
751 nsAutoScriptBlocker scriptBlocker
;
752 AutoRestore
<bool> saveAllowFlushingLayout(mLayoutFlusher
.mAllowFlushing
);
753 mLayoutFlusher
.mAllowFlushing
= false;
755 Maybe
<PresShell::AutoAssertNoFlush
> assert;
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)",
766 UpdateCarets({UpdateCaretsHint::RespectOldAppearance
,
767 UpdateCaretsHint::DispatchNoEvent
});
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;
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();
817 return fs
->GetSelection(SelectionType::eNormal
);
820 already_AddRefed
<nsFrameSelection
> AccessibleCaretManager::GetFrameSelection()
826 // Prevent us from touching the nsFrameSelection associated with other
828 RefPtr
<nsFrameSelection
> fs
= mPresShell
->GetLastFocusedFrameSelection();
829 if (!fs
|| fs
->GetPresShell() != mPresShell
) {
836 nsAutoString
AccessibleCaretManager::StringifiedSelection() const {
838 RefPtr
<Selection
> selection
= GetSelection();
840 selection
->Stringify(str
, mLayoutFlusher
.mAllowFlushing
841 ? Selection::FlushFrames::Yes
842 : Selection::FlushFrames::No
);
848 Element
* AccessibleCaretManager::GetEditingHostForFrame(
849 const nsIFrame
* aFrame
) {
854 auto content
= aFrame
->GetContent();
859 return content
->GetEditingHost();
862 AccessibleCaretManager::CaretMode
AccessibleCaretManager::GetCaretMode() const {
863 const Selection
* selection
= GetSelection();
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();
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)) {
895 focusableFrame
= focusableFrame
->GetParent();
897 return focusableFrame
;
900 void AccessibleCaretManager::ChangeFocusToOrClearOldFocus(
901 nsIFrame
* aFrame
) const {
902 RefPtr
<nsFocusManager
> fm
= nsFocusManager::GetFocusManager();
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()) {
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();
937 void AccessibleCaretManager::SetSelectionDragState(bool aState
) const {
938 RefPtr
<nsFrameSelection
> fs
= GetFrameSelection();
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
)
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 {
970 // Extend the phone number selection until we find a boundary.
971 RefPtr
<Selection
> selection
= GetSelection();
974 const nsRange
* anchorFocusRange
= selection
->GetAnchorFocusRange();
975 if (!anchorFocusRange
) {
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
,
992 if (IsTerminated() == Terminated::Yes
) {
996 // If the selection didn't change, (can't extend further), we're done.
997 if (selection
->GetFocusNode() == oldFocusNode
&&
998 selection
->FocusOffset() == oldFocusOffset
) {
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
);
1018 void AccessibleCaretManager::SetSelectionDirection(nsDirection aDir
) const {
1019 Selection
* selection
= GetSelection();
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();
1030 fs
->MaintainSelection(eSelectNoAmount
);
1034 void AccessibleCaretManager::LayoutFlusher::MaybeFlush(
1035 const PresShell
& aPresShell
) {
1036 if (mAllowFlushing
) {
1037 AutoRestore
<bool> flushing(mFlushing
);
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 {
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
;
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
);
1089 RefPtr
<TreeWalker
> walker
= mPresShell
->GetDocument()->CreateTreeWalker(
1090 *startNode
, dom::NodeFilter_Binding::SHOW_ALL
, nullptr, err
);
1096 startFrame
= startContent
? startContent
->GetPrimaryFrame() : nullptr;
1097 while (!startFrame
&& startNode
!= endNode
) {
1098 startNode
= findInFirstRangeStart
? walker
->NextNode(err
)
1099 : walker
->PreviousNode(err
);
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.
1117 startContent
.forget(aOutContent
);
1119 if (aOutContentOffset
) {
1120 *aOutContentOffset
= nodeOffset
;
1127 bool AccessibleCaretManager::RestrictCaretDraggingOffsets(
1128 nsIFrame::ContentOffsets
& aOffsets
) {
1133 MOZ_ASSERT(GetCaretMode() == CaretMode::Selection
);
1136 mActiveCaret
== mCarets
.GetFirst() ? eDirPrevious
: eDirNext
;
1138 nsCOMPtr
<nsIContent
> content
;
1139 int32_t contentOffset
= 0;
1140 nsIFrame
* frame
= GetFrameForFirstRangeStartOrLastRangeEnd(
1141 dir
, &offset
, getter_AddRefs(content
), &contentOffset
);
1147 // Compare the active caret's new position (aOffsets) to the inactive caret's
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).
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).
1185 auto SetOffsetsToLimit
= [&aOffsets
, &limit
]() {
1186 aOffsets
.content
= limit
.mResultContent
;
1187 aOffsets
.offset
= limit
.mContentOffset
;
1188 aOffsets
.secondaryOffset
= limit
.mContentOffset
;
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
1198 SetOffsetsToLimit();
1201 switch (*cmpToInactiveCaretPos
) {
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();
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();
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();
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());
1248 return NS_ERROR_FAILURE
;
1251 RefPtr
<nsFrameSelection
> fs
= GetFrameSelection();
1255 nsIFrame
* newFrame
= nullptr;
1257 nsPoint ptInFrame
= point
;
1258 nsLayoutUtils::TransformPoint(RelativeTo
{rootFrame
}, RelativeTo
{ptFrame
},
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
,
1294 nsRect
AccessibleCaretManager::GetAllChildFrameRectsUnion(nsIFrame
* aFrame
) {
1297 // Drill through scroll frames, we don't want to include scrollbar child
1299 for (nsIFrame
* frame
= aFrame
->GetContentInsertionFrame(); frame
;
1300 frame
= frame
->GetNextContinuation()) {
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
);
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
);
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
);
1342 nsIFrame
* editingHostFrame
= editingHost
->GetPrimaryFrame();
1343 if (editingHostFrame
) {
1345 AccessibleCaretManager::GetAllChildFrameRectsUnion(editingHostFrame
);
1346 nsLayoutUtils::TransformRect(editingHostFrame
, mPresShell
->GetRootFrame(),
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
&&
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
1367 if (mActiveCaret
== mCarets
.GetFirst()) {
1368 nscoord dragDownBoundaryY
= mCarets
.GetSecond()->LogicalPosition().y
;
1369 if (dragDownBoundaryY
> 0 && adjustedPoint
.y
> dragDownBoundaryY
) {
1370 adjustedPoint
.y
= dragDownBoundaryY
;
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();
1393 nsIScrollableFrame
* scrollFrame
= nsLayoutUtils::GetNearestScrollableFrame(
1394 anchorFrame
, nsLayoutUtils::SCROLLABLE_SAME_DOC
|
1395 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN
);
1400 nsIFrame
* capturingFrame
= scrollFrame
->GetScrolledFrame();
1401 if (!capturingFrame
) {
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();
1413 fs
->StartAutoScrollTimer(capturingFrame
, ptInScrolled
, kAutoScrollTimerDelay
);
1416 void AccessibleCaretManager::StopSelectionAutoScrollTimer() const {
1417 RefPtr
<nsFrameSelection
> fs
= GetFrameSelection();
1419 fs
->StopAutoScrollTimer();
1422 void AccessibleCaretManager::DispatchCaretStateChangedEvent(
1423 CaretChangedReason aReason
, const nsPoint
* aPoint
) {
1424 if (MaybeFlushLayout() == Terminated::Yes
) {
1428 const Selection
* sel
= GetSelection();
1433 Document
* doc
= mPresShell
->GetDocument();
1436 CaretStateChangedEventInit init
;
1437 init
.mBubbles
= true;
1439 const nsRange
* range
= sel
->GetAnchorFocusRange();
1440 nsINode
* commonAncestorNode
= nullptr;
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
);
1465 init
.mSelectionVisible
= !clampedRect
.IsEmpty();
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();
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
))
1502 AccessibleCaretManager::Carets::Carets(UniquePtr
<AccessibleCaret
> aFirst
,
1503 UniquePtr
<AccessibleCaret
> aSecond
)
1504 : mFirst
{std::move(aFirst
)}, mSecond
{std::move(aSecond
)} {}
1506 } // namespace mozilla