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/. */
6 
7 #include "AccessibleCaretManager.h"
8 
9 #include <utility>
10 
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"
26 #include "nsCaret.h"
27 #include "nsContainerFrame.h"
28 #include "nsContentUtils.h"
29 #include "nsDebug.h"
30 #include "nsFocusManager.h"
31 #include "nsIFrame.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"
38 
39 namespace mozilla {
40 
41 #undef AC_LOG
42 #define AC_LOG(message, ...) \
43   AC_LOG_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
44 
45 #undef AC_LOGV
46 #define AC_LOGV(message, ...) \
47   AC_LOGV_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
48 
49 using namespace dom;
50 using Appearance = AccessibleCaret::Appearance;
51 using PositionChangedResult = AccessibleCaret::PositionChangedResult;
52 
53 #define AC_PROCESS_ENUM_TO_STREAM(e) \
54   case (e):                          \
55     aStream << #e;                   \
56     break;
operator <<(std::ostream & aStream,const AccessibleCaretManager::CaretMode & aCaretMode)57 std::ostream& operator<<(std::ostream& aStream,
58                          const AccessibleCaretManager::CaretMode& aCaretMode) {
59   using CaretMode = AccessibleCaretManager::CaretMode;
60   switch (aCaretMode) {
61     AC_PROCESS_ENUM_TO_STREAM(CaretMode::None);
62     AC_PROCESS_ENUM_TO_STREAM(CaretMode::Cursor);
63     AC_PROCESS_ENUM_TO_STREAM(CaretMode::Selection);
64   }
65   return aStream;
66 }
67 
operator <<(std::ostream & aStream,const AccessibleCaretManager::UpdateCaretsHint & aHint)68 std::ostream& operator<<(
69     std::ostream& aStream,
70     const AccessibleCaretManager::UpdateCaretsHint& aHint) {
71   using UpdateCaretsHint = AccessibleCaretManager::UpdateCaretsHint;
72   switch (aHint) {
73     AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::Default);
74     AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::RespectOldAppearance);
75     AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::DispatchNoEvent);
76   }
77   return aStream;
78 }
79 #undef AC_PROCESS_ENUM_TO_STREAM
80 
AccessibleCaretManager(PresShell * aPresShell)81 AccessibleCaretManager::AccessibleCaretManager(PresShell* aPresShell)
82     : AccessibleCaretManager{
83           aPresShell,
84           Carets{aPresShell ? MakeUnique<AccessibleCaret>(aPresShell) : nullptr,
85                  aPresShell ? MakeUnique<AccessibleCaret>(aPresShell)
86                             : nullptr}} {}
87 
AccessibleCaretManager(PresShell * aPresShell,Carets aCarets)88 AccessibleCaretManager::AccessibleCaretManager(PresShell* aPresShell,
89                                                Carets aCarets)
90     : mPresShell{aPresShell}, mCarets{std::move(aCarets)} {}
91 
~LayoutFlusher()92 AccessibleCaretManager::LayoutFlusher::~LayoutFlusher() {
93   MOZ_RELEASE_ASSERT(!mFlushing, "Going away in MaybeFlush? Bad!");
94 }
95 
Terminate()96 void AccessibleCaretManager::Terminate() {
97   mCarets.Terminate();
98   mActiveCaret = nullptr;
99   mPresShell = nullptr;
100 }
101 
OnSelectionChanged(Document * aDoc,Selection * aSel,int16_t aReason)102 nsresult AccessibleCaretManager::OnSelectionChanged(Document* aDoc,
103                                                     Selection* aSel,
104                                                     int16_t aReason) {
105   Selection* selection = GetSelection();
106   AC_LOG("%s: aSel: %p, GetSelection(): %p, aReason: %d", __FUNCTION__, aSel,
107          selection, aReason);
108   if (aSel != selection) {
109     return NS_OK;
110   }
111 
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) {
117     return NS_OK;
118   }
119 
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())) {
127       UpdateCarets();
128       return NS_OK;
129     }
130     // Default for NO_REASON is to make hidden.
131     HideCaretsAndDispatchCaretStateChangedEvent();
132     return NS_OK;
133   }
134 
135   // Move cursor by keyboard.
136   if (aReason & nsISelectionListener::KEYPRESS_REASON) {
137     HideCaretsAndDispatchCaretStateChangedEvent();
138     return NS_OK;
139   }
140 
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();
145     return NS_OK;
146   }
147 
148   // Range will collapse after cutting or copying text.
149   if (aReason & (nsISelectionListener::COLLAPSETOSTART_REASON |
150                  nsISelectionListener::COLLAPSETOEND_REASON)) {
151     HideCaretsAndDispatchCaretStateChangedEvent();
152     return NS_OK;
153   }
154 
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();
159     return NS_OK;
160   }
161 
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();
168     return NS_OK;
169   }
170 
171   UpdateCarets();
172   return NS_OK;
173 }
174 
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);
182   }
183 }
184 
MaybeFlushLayout()185 auto AccessibleCaretManager::MaybeFlushLayout() -> Terminated {
186   if (mPresShell) {
187     // `MaybeFlush` doesn't access the PresShell after flushing, so it's OK to
188     // mark it as live.
189     mLayoutFlusher.MaybeFlush(MOZ_KnownLive(*mPresShell));
190   }
191 
192   return IsTerminated();
193 }
194 
UpdateCarets(const UpdateCaretsHintSet & aHint)195 void AccessibleCaretManager::UpdateCarets(const UpdateCaretsHintSet& aHint) {
196   if (MaybeFlushLayout() == Terminated::Yes) {
197     return;
198   }
199 
200   mLastUpdateCaretMode = GetCaretMode();
201 
202   switch (mLastUpdateCaretMode) {
203     case CaretMode::None:
204       HideCaretsAndDispatchCaretStateChangedEvent();
205       break;
206     case CaretMode::Cursor:
207       UpdateCaretsForCursorMode(aHint);
208       break;
209     case CaretMode::Selection:
210       UpdateCaretsForSelectionMode(aHint);
211       break;
212   }
213 
214   mDesiredAsyncPanZoomState.Update(*this);
215 }
216 
IsCaretDisplayableInCursorMode(nsIFrame ** aOutFrame,int32_t * aOutOffset) const217 bool AccessibleCaretManager::IsCaretDisplayableInCursorMode(
218     nsIFrame** aOutFrame, int32_t* aOutOffset) const {
219   RefPtr<nsCaret> caret = mPresShell->GetCaret();
220   if (!caret || !caret->IsVisible()) {
221     return false;
222   }
223 
224   int32_t offset = 0;
225   nsIFrame* frame =
226       nsCaret::GetFrameAndOffset(GetSelection(), nullptr, 0, &offset);
227 
228   if (!frame) {
229     return false;
230   }
231 
232   if (!GetEditingHostForFrame(frame)) {
233     return false;
234   }
235 
236   if (aOutFrame) {
237     *aOutFrame = frame;
238   }
239 
240   if (aOutOffset) {
241     *aOutOffset = offset;
242   }
243 
244   return true;
245 }
246 
HasNonEmptyTextContent(nsINode * aNode) const247 bool AccessibleCaretManager::HasNonEmptyTextContent(nsINode* aNode) const {
248   return nsContentUtils::HasNonEmptyTextContent(
249       aNode, nsContentUtils::eRecurseIntoChildren);
250 }
251 
UpdateCaretsForCursorMode(const UpdateCaretsHintSet & aHints)252 void AccessibleCaretManager::UpdateCaretsForCursorMode(
253     const UpdateCaretsHintSet& aHints) {
254   AC_LOG("%s, selection: %p", __FUNCTION__, GetSelection());
255 
256   int32_t offset = 0;
257   nsIFrame* frame = nullptr;
258   if (!IsCaretDisplayableInCursorMode(&frame, &offset)) {
259     HideCaretsAndDispatchCaretStateChangedEvent();
260     return;
261   }
262 
263   PositionChangedResult result = mCarets.GetFirst()->SetPosition(frame, offset);
264 
265   switch (result) {
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);
272         } else if (
273             StaticPrefs::
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);
280           } else {
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.
285             //
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.
289           }
290         } else {
291           mCarets.GetFirst()->SetAppearance(Appearance::NormalNotShown);
292         }
293       }
294       break;
295 
296     case PositionChangedResult::Invisible:
297       mCarets.GetFirst()->SetAppearance(Appearance::NormalNotShown);
298       break;
299   }
300 
301   mCarets.GetSecond()->SetAppearance(Appearance::None);
302 
303   mIsCaretPositionChanged = (result == PositionChangedResult::Position);
304 
305   if (!aHints.contains(UpdateCaretsHint::DispatchNoEvent) && !mActiveCaret) {
306     DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition);
307   }
308 }
309 
UpdateCaretsForSelectionMode(const UpdateCaretsHintSet & aHints)310 void AccessibleCaretManager::UpdateCaretsForSelectionMode(
311     const UpdateCaretsHintSet& aHints) {
312   AC_LOG("%s: selection: %p", __FUNCTION__, GetSelection());
313 
314   int32_t startOffset = 0;
315   nsIFrame* startFrame =
316       GetFrameForFirstRangeStartOrLastRangeEnd(eDirNext, &startOffset);
317 
318   int32_t endOffset = 0;
319   nsIFrame* endFrame =
320       GetFrameForFirstRangeStartOrLastRangeEnd(eDirPrevious, &endOffset);
321 
322   if (!CompareTreePosition(startFrame, endFrame)) {
323     // XXX: Do we really have to hide carets if this condition isn't satisfied?
324     HideCaretsAndDispatchCaretStateChangedEvent();
325     return;
326   }
327 
328   auto updateSingleCaret = [aHints](AccessibleCaret* aCaret, nsIFrame* aFrame,
329                                     int32_t aOffset) -> PositionChangedResult {
330     PositionChangedResult result = aCaret->SetPosition(aFrame, aOffset);
331 
332     switch (result) {
333       case PositionChangedResult::NotChanged:
334       case PositionChangedResult::Position:
335       case PositionChangedResult::Zoom:
336         if (!aHints.contains(UpdateCaretsHint::RespectOldAppearance)) {
337           aCaret->SetAppearance(Appearance::Normal);
338         }
339         break;
340 
341       case PositionChangedResult::Invisible:
342         aCaret->SetAppearance(Appearance::NormalNotShown);
343         break;
344     }
345     return result;
346   };
347 
348   PositionChangedResult firstCaretResult =
349       updateSingleCaret(mCarets.GetFirst(), startFrame, startOffset);
350   PositionChangedResult secondCaretResult =
351       updateSingleCaret(mCarets.GetSecond(), endFrame, endOffset);
352 
353   mIsCaretPositionChanged =
354       firstCaretResult == PositionChangedResult::Position ||
355       secondCaretResult == PositionChangedResult::Position;
356 
357   if (mIsCaretPositionChanged) {
358     // Flush layout to make the carets intersection correct.
359     if (MaybeFlushLayout() == Terminated::Yes) {
360       return;
361     }
362   }
363 
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
367     // caller.
368     if (StaticPrefs::layout_accessiblecaret_always_tilt()) {
369       UpdateCaretsForAlwaysTilt(startFrame, endFrame);
370     } else {
371       UpdateCaretsForOverlappingTilt();
372     }
373   }
374 
375   if (!aHints.contains(UpdateCaretsHint::DispatchNoEvent) && !mActiveCaret) {
376     DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition);
377   }
378 }
379 
Update(const AccessibleCaretManager & aAccessibleCaretManager)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;
385     return;
386   }
387 
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
392                                                              : Value::Enabled;
393     return;
394   }
395 
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;
401       break;
402     case CaretMode::Cursor:
403       mValue =
404           (aAccessibleCaretManager.mCarets.GetFirst()->IsVisuallyVisible() &&
405            aAccessibleCaretManager.mCarets.GetFirst()
406                ->IsInPositionFixedSubtree())
407               ? Value::Disabled
408               : Value::Enabled;
409       break;
410     case CaretMode::Selection:
411       mValue =
412           ((aAccessibleCaretManager.mCarets.GetFirst()->IsVisuallyVisible() &&
413             aAccessibleCaretManager.mCarets.GetFirst()
414                 ->IsInPositionFixedSubtree()) ||
415            (aAccessibleCaretManager.mCarets.GetSecond()->IsVisuallyVisible() &&
416             aAccessibleCaretManager.mCarets.GetSecond()
417                 ->IsInPositionFixedSubtree()))
418               ? Value::Disabled
419               : Value::Enabled;
420       break;
421   }
422 }
423 
UpdateCaretsForOverlappingTilt()424 bool AccessibleCaretManager::UpdateCaretsForOverlappingTilt() {
425   if (!mCarets.GetFirst()->IsVisuallyVisible() ||
426       !mCarets.GetSecond()->IsVisuallyVisible()) {
427     return false;
428   }
429 
430   if (!mCarets.GetFirst()->Intersects(*mCarets.GetSecond())) {
431     mCarets.GetFirst()->SetAppearance(Appearance::Normal);
432     mCarets.GetSecond()->SetAppearance(Appearance::Normal);
433     return false;
434   }
435 
436   if (mCarets.GetFirst()->LogicalPosition().x <=
437       mCarets.GetSecond()->LogicalPosition().x) {
438     mCarets.GetFirst()->SetAppearance(Appearance::Left);
439     mCarets.GetSecond()->SetAppearance(Appearance::Right);
440   } else {
441     mCarets.GetFirst()->SetAppearance(Appearance::Right);
442     mCarets.GetSecond()->SetAppearance(Appearance::Left);
443   }
444 
445   return true;
446 }
447 
UpdateCaretsForAlwaysTilt(const nsIFrame * aStartFrame,const nsIFrame * aEndFrame)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()) {
453     return;
454   }
455 
456   if (mCarets.GetFirst()->IsVisuallyVisible()) {
457     auto startFrameWritingMode = aStartFrame->GetWritingMode();
458     mCarets.GetFirst()->SetAppearance(startFrameWritingMode.IsBidiLTR()
459                                           ? Appearance::Left
460                                           : Appearance::Right);
461   }
462   if (mCarets.GetSecond()->IsVisuallyVisible()) {
463     auto endFrameWritingMode = aEndFrame->GetWritingMode();
464     mCarets.GetSecond()->SetAppearance(
465         endFrameWritingMode.IsBidiLTR() ? Appearance::Right : Appearance::Left);
466   }
467 }
468 
ProvideHapticFeedback()469 void AccessibleCaretManager::ProvideHapticFeedback() {
470   if (StaticPrefs::layout_accessiblecaret_hapticfeedback()) {
471     nsCOMPtr<nsIHapticFeedback> haptic =
472         do_GetService("@mozilla.org/widget/hapticfeedback;1");
473     haptic->PerformSimpleAction(haptic->LongPress);
474   }
475 }
476 
PressCaret(const nsPoint & aPoint,EventClassID aEventClass)477 nsresult AccessibleCaretManager::PressCaret(const nsPoint& aPoint,
478                                             EventClassID aEventClass) {
479   nsresult rv = NS_ERROR_FAILURE;
480 
481   MOZ_ASSERT(aEventClass == eMouseEventClass || aEventClass == eTouchEventClass,
482              "Unexpected event class!");
483 
484   using TouchArea = AccessibleCaret::TouchArea;
485   TouchArea touchArea =
486       aEventClass == eMouseEventClass ? TouchArea::CaretImage : TouchArea::Full;
487 
488   if (mCarets.GetFirst()->Contains(aPoint, touchArea)) {
489     mActiveCaret = mCarets.GetFirst();
490     SetSelectionDirection(eDirPrevious);
491   } else if (mCarets.GetSecond()->Contains(aPoint, touchArea)) {
492     mActiveCaret = mCarets.GetSecond();
493     SetSelectionDirection(eDirNext);
494   }
495 
496   if (mActiveCaret) {
497     mOffsetYToCaretLogicalPosition =
498         mActiveCaret->LogicalPosition().y - aPoint.y;
499     SetSelectionDragState(true);
500     DispatchCaretStateChangedEvent(CaretChangedReason::Presscaret);
501     rv = NS_OK;
502   }
503 
504   return rv;
505 }
506 
DragCaret(const nsPoint & aPoint)507 nsresult AccessibleCaretManager::DragCaret(const nsPoint& aPoint) {
508   MOZ_ASSERT(mActiveCaret);
509   MOZ_ASSERT(GetCaretMode() != CaretMode::None);
510 
511   if (!mPresShell || !mPresShell->GetRootFrame() || !GetSelection()) {
512     return NS_ERROR_NULL_POINTER;
513   }
514 
515   StopSelectionAutoScrollTimer();
516   DragCaretInternal(aPoint);
517 
518   // We want to scroll the page even if we failed to drag the caret.
519   StartSelectionAutoScrollTimer(aPoint);
520   UpdateCarets();
521   return NS_OK;
522 }
523 
ReleaseCaret()524 nsresult AccessibleCaretManager::ReleaseCaret() {
525   MOZ_ASSERT(mActiveCaret);
526 
527   mActiveCaret = nullptr;
528   SetSelectionDragState(false);
529   mDesiredAsyncPanZoomState.Update(*this);
530   DispatchCaretStateChangedEvent(CaretChangedReason::Releasecaret);
531   return NS_OK;
532 }
533 
TapCaret(const nsPoint & aPoint)534 nsresult AccessibleCaretManager::TapCaret(const nsPoint& aPoint) {
535   MOZ_ASSERT(GetCaretMode() != CaretMode::None);
536 
537   nsresult rv = NS_ERROR_FAILURE;
538 
539   if (GetCaretMode() == CaretMode::Cursor) {
540     DispatchCaretStateChangedEvent(CaretChangedReason::Taponcaret);
541     rv = NS_OK;
542   }
543 
544   return rv;
545 }
546 
GetHitTestOptions()547 static EnumSet<nsLayoutUtils::FrameForPointOption> GetHitTestOptions() {
548   EnumSet<nsLayoutUtils::FrameForPointOption> options = {
549       nsLayoutUtils::FrameForPointOption::IgnorePaintSuppression,
550       nsLayoutUtils::FrameForPointOption::IgnoreCrossDoc};
551   return options;
552 }
553 
SelectWordOrShortcut(const nsPoint & aPoint)554 nsresult AccessibleCaretManager::SelectWordOrShortcut(const nsPoint& aPoint) {
555   // If the long-tap is landing on a pre-existing selection, don't replace
556   // it with a new one. Instead just return and let the context menu pop up
557   // on the pre-existing selection.
558   if (GetCaretMode() == CaretMode::Selection &&
559       GetSelection()->ContainsPoint(aPoint)) {
560     AC_LOG("%s: UpdateCarets() for current selection", __FUNCTION__);
561     UpdateCarets();
562     ProvideHapticFeedback();
563     return NS_OK;
564   }
565 
566   if (!mPresShell) {
567     return NS_ERROR_UNEXPECTED;
568   }
569 
570   nsIFrame* rootFrame = mPresShell->GetRootFrame();
571   if (!rootFrame) {
572     return NS_ERROR_NOT_AVAILABLE;
573   }
574 
575   // Find the frame under point.
576   AutoWeakFrame ptFrame = nsLayoutUtils::GetFrameForPoint(
577       RelativeTo{rootFrame}, aPoint, GetHitTestOptions());
578   if (!ptFrame.GetFrame()) {
579     return NS_ERROR_FAILURE;
580   }
581 
582   nsIFrame* focusableFrame = GetFocusableFrame(ptFrame);
583 
584 #ifdef DEBUG_FRAME_DUMP
585   AC_LOG("%s: Found %s under (%d, %d)", __FUNCTION__, ptFrame->ListTag().get(),
586          aPoint.x, aPoint.y);
587   AC_LOG("%s: Found %s focusable", __FUNCTION__,
588          focusableFrame ? focusableFrame->ListTag().get() : "no frame");
589 #endif
590 
591   // Get ptInFrame here so that we don't need to check whether rootFrame is
592   // alive later. Note that if ptFrame is being moved by
593   // IMEStateManager::NotifyIME() or ChangeFocusToOrClearOldFocus() below,
594   // something under the original point will be selected, which may not be the
595   // original text the user wants to select.
596   nsPoint ptInFrame = aPoint;
597   nsLayoutUtils::TransformPoint(RelativeTo{rootFrame}, RelativeTo{ptFrame},
598                                 ptInFrame);
599 
600   // Firstly check long press on an empty editable content.
601   Element* newFocusEditingHost = GetEditingHostForFrame(ptFrame);
602   if (focusableFrame && newFocusEditingHost &&
603       !HasNonEmptyTextContent(newFocusEditingHost)) {
604     ChangeFocusToOrClearOldFocus(focusableFrame);
605 
606     if (StaticPrefs::
607             layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
608       mCarets.GetFirst()->SetAppearance(Appearance::Normal);
609     }
610     // We need to update carets to get correct information before dispatching
611     // CaretStateChangedEvent.
612     UpdateCarets();
613     ProvideHapticFeedback();
614     DispatchCaretStateChangedEvent(CaretChangedReason::Longpressonemptycontent);
615     return NS_OK;
616   }
617 
618   bool selectable = ptFrame->IsSelectable(nullptr);
619 
620 #ifdef DEBUG_FRAME_DUMP
621   AC_LOG("%s: %s %s selectable.", __FUNCTION__, ptFrame->ListTag().get(),
622          selectable ? "is" : "is NOT");
623 #endif
624 
625   if (!selectable) {
626     return NS_ERROR_FAILURE;
627   }
628 
629   // Commit the composition string of the old editable focus element (if there
630   // is any) before changing the focus.
631   IMEStateManager::NotifyIME(widget::REQUEST_TO_COMMIT_COMPOSITION,
632                              mPresShell->GetPresContext());
633   if (!ptFrame.IsAlive()) {
634     // Cannot continue because ptFrame died.
635     return NS_ERROR_FAILURE;
636   }
637 
638   // ptFrame is selectable. Now change the focus.
639   ChangeFocusToOrClearOldFocus(focusableFrame);
640   if (!ptFrame.IsAlive()) {
641     // Cannot continue because ptFrame died.
642     return NS_ERROR_FAILURE;
643   }
644 
645   // If long tap point isn't selectable frame for caret and frame selection
646   // can find a better frame for caret, we don't select a word.
647   // See https://webcompat.com/issues/15953
648   nsIFrame::ContentOffsets offsets =
649       ptFrame->GetContentOffsetsFromPoint(ptInFrame, nsIFrame::SKIP_HIDDEN);
650   if (offsets.content) {
651     RefPtr<nsFrameSelection> frameSelection = GetFrameSelection();
652     if (frameSelection) {
653       int32_t offset;
654       nsIFrame* theFrame = nsFrameSelection::GetFrameForNodeOffset(
655           offsets.content, offsets.offset, offsets.associate, &offset);
656       if (theFrame && theFrame != ptFrame) {
657         SetSelectionDragState(true);
658         frameSelection->HandleClick(
659             MOZ_KnownLive(offsets.content) /* bug 1636889 */,
660             offsets.StartOffset(), offsets.EndOffset(),
661             nsFrameSelection::FocusMode::kCollapseToNewPoint,
662             offsets.associate);
663         SetSelectionDragState(false);
664         ClearMaintainedSelection();
665 
666         if (StaticPrefs::
667                 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
668           mCarets.GetFirst()->SetAppearance(Appearance::Normal);
669         }
670 
671         UpdateCarets();
672         ProvideHapticFeedback();
673         DispatchCaretStateChangedEvent(
674             CaretChangedReason::Longpressonemptycontent);
675 
676         return NS_OK;
677       }
678     }
679   }
680 
681   // Then try select a word under point.
682   nsresult rv = SelectWord(ptFrame, ptInFrame);
683   UpdateCarets();
684   ProvideHapticFeedback();
685 
686   return rv;
687 }
688 
OnScrollStart()689 void AccessibleCaretManager::OnScrollStart() {
690   AC_LOG("%s", __FUNCTION__);
691 
692   AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing);
693   mLayoutFlusher.mAllowFlushing = false;
694 
695   Maybe<PresShell::AutoAssertNoFlush> assert;
696   if (mPresShell) {
697     assert.emplace(*mPresShell);
698   }
699 
700   mIsScrollStarted = true;
701 
702   if (mCarets.HasLogicallyVisibleCaret()) {
703     // Dispatch the event only if one of the carets is logically visible like in
704     // HideCaretsAndDispatchCaretStateChangedEvent().
705     DispatchCaretStateChangedEvent(CaretChangedReason::Scroll);
706   }
707 }
708 
OnScrollEnd()709 void AccessibleCaretManager::OnScrollEnd() {
710   AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing);
711   mLayoutFlusher.mAllowFlushing = false;
712 
713   Maybe<PresShell::AutoAssertNoFlush> assert;
714   if (mPresShell) {
715     assert.emplace(*mPresShell);
716   }
717 
718   mIsScrollStarted = false;
719 
720   if (GetCaretMode() == CaretMode::Cursor) {
721     if (!mCarets.GetFirst()->IsLogicallyVisible()) {
722       // If the caret is hidden (Appearance::None) due to blur, no
723       // need to update it.
724       return;
725     }
726   }
727 
728   // For mouse input we don't want to show the carets.
729   if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
730       mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_MOUSE) {
731     AC_LOG("%s: HideCaretsAndDispatchCaretStateChangedEvent()", __FUNCTION__);
732     HideCaretsAndDispatchCaretStateChangedEvent();
733     return;
734   }
735 
736   AC_LOG("%s: UpdateCarets()", __FUNCTION__);
737   UpdateCarets();
738 }
739 
OnScrollPositionChanged()740 void AccessibleCaretManager::OnScrollPositionChanged() {
741   AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing);
742   mLayoutFlusher.mAllowFlushing = false;
743 
744   Maybe<PresShell::AutoAssertNoFlush> assert;
745   if (mPresShell) {
746     assert.emplace(*mPresShell);
747   }
748 
749   if (mCarets.HasLogicallyVisibleCaret()) {
750     if (mIsScrollStarted) {
751       // We don't want extra CaretStateChangedEvents dispatched when user is
752       // scrolling the page.
753       AC_LOG("%s: UpdateCarets(RespectOldAppearance | DispatchNoEvent)",
754              __FUNCTION__);
755       UpdateCarets({UpdateCaretsHint::RespectOldAppearance,
756                     UpdateCaretsHint::DispatchNoEvent});
757     } else {
758       AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__);
759       UpdateCarets(UpdateCaretsHint::RespectOldAppearance);
760     }
761   }
762 }
763 
OnReflow()764 void AccessibleCaretManager::OnReflow() {
765   AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing);
766   mLayoutFlusher.mAllowFlushing = false;
767 
768   Maybe<PresShell::AutoAssertNoFlush> assert;
769   if (mPresShell) {
770     assert.emplace(*mPresShell);
771   }
772 
773   if (mCarets.HasLogicallyVisibleCaret()) {
774     AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__);
775     UpdateCarets(UpdateCaretsHint::RespectOldAppearance);
776   }
777 }
778 
OnBlur()779 void AccessibleCaretManager::OnBlur() {
780   AC_LOG("%s: HideCaretsAndDispatchCaretStateChangedEvent()", __FUNCTION__);
781   HideCaretsAndDispatchCaretStateChangedEvent();
782 }
783 
OnKeyboardEvent()784 void AccessibleCaretManager::OnKeyboardEvent() {
785   if (GetCaretMode() == CaretMode::Cursor) {
786     AC_LOG("%s: HideCaretsAndDispatchCaretStateChangedEvent()", __FUNCTION__);
787     HideCaretsAndDispatchCaretStateChangedEvent();
788   }
789 }
790 
OnFrameReconstruction()791 void AccessibleCaretManager::OnFrameReconstruction() {
792   mCarets.GetFirst()->EnsureApzAware();
793   mCarets.GetSecond()->EnsureApzAware();
794 }
795 
SetLastInputSource(uint16_t aInputSource)796 void AccessibleCaretManager::SetLastInputSource(uint16_t aInputSource) {
797   mLastInputSource = aInputSource;
798 }
799 
ShouldDisableApz() const800 bool AccessibleCaretManager::ShouldDisableApz() const {
801   return mDesiredAsyncPanZoomState.Get() ==
802          DesiredAsyncPanZoomState::Value::Disabled;
803 }
804 
GetSelection() const805 Selection* AccessibleCaretManager::GetSelection() const {
806   RefPtr<nsFrameSelection> fs = GetFrameSelection();
807   if (!fs) {
808     return nullptr;
809   }
810   return fs->GetSelection(SelectionType::eNormal);
811 }
812 
GetFrameSelection() const813 already_AddRefed<nsFrameSelection> AccessibleCaretManager::GetFrameSelection()
814     const {
815   if (!mPresShell) {
816     return nullptr;
817   }
818 
819   nsFocusManager* fm = nsFocusManager::GetFocusManager();
820   MOZ_ASSERT(fm);
821 
822   nsIContent* focusedContent = fm->GetFocusedElement();
823   if (!focusedContent) {
824     // For non-editable content
825     return mPresShell->FrameSelection();
826   }
827 
828   nsIFrame* focusFrame = focusedContent->GetPrimaryFrame();
829   if (!focusFrame) {
830     return nullptr;
831   }
832 
833   // Prevent us from touching the nsFrameSelection associated with other
834   // PresShell.
835   RefPtr<nsFrameSelection> fs = focusFrame->GetFrameSelection();
836   if (!fs || fs->GetPresShell() != mPresShell) {
837     return nullptr;
838   }
839 
840   return fs.forget();
841 }
842 
StringifiedSelection() const843 nsAutoString AccessibleCaretManager::StringifiedSelection() const {
844   nsAutoString str;
845   RefPtr<Selection> selection = GetSelection();
846   if (selection) {
847     selection->Stringify(str, mLayoutFlusher.mAllowFlushing
848                                   ? Selection::FlushFrames::Yes
849                                   : Selection::FlushFrames::No);
850   }
851   return str;
852 }
853 
854 // static
GetEditingHostForFrame(const nsIFrame * aFrame)855 Element* AccessibleCaretManager::GetEditingHostForFrame(
856     const nsIFrame* aFrame) {
857   if (!aFrame) {
858     return nullptr;
859   }
860 
861   auto content = aFrame->GetContent();
862   if (!content) {
863     return nullptr;
864   }
865 
866   return content->GetEditingHost();
867 }
868 
GetCaretMode() const869 AccessibleCaretManager::CaretMode AccessibleCaretManager::GetCaretMode() const {
870   const Selection* selection = GetSelection();
871   if (!selection) {
872     return CaretMode::None;
873   }
874 
875   const uint32_t rangeCount = selection->RangeCount();
876   if (rangeCount <= 0) {
877     return CaretMode::None;
878   }
879 
880   const nsFocusManager* fm = nsFocusManager::GetFocusManager();
881   MOZ_ASSERT(fm);
882   if (fm->GetFocusedWindow() != mPresShell->GetDocument()->GetWindow()) {
883     // Hide carets if the window is not focused.
884     return CaretMode::None;
885   }
886 
887   if (selection->IsCollapsed()) {
888     return CaretMode::Cursor;
889   }
890 
891   return CaretMode::Selection;
892 }
893 
GetFocusableFrame(nsIFrame * aFrame) const894 nsIFrame* AccessibleCaretManager::GetFocusableFrame(nsIFrame* aFrame) const {
895   // This implementation is similar to EventStateManager::PostHandleEvent().
896   // Look for the nearest enclosing focusable frame.
897   nsIFrame* focusableFrame = aFrame;
898   while (focusableFrame) {
899     if (focusableFrame->IsFocusable(/* aWithMouse = */ true)) {
900       break;
901     }
902     focusableFrame = focusableFrame->GetParent();
903   }
904   return focusableFrame;
905 }
906 
ChangeFocusToOrClearOldFocus(nsIFrame * aFrame) const907 void AccessibleCaretManager::ChangeFocusToOrClearOldFocus(
908     nsIFrame* aFrame) const {
909   RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager();
910   MOZ_ASSERT(fm);
911 
912   if (aFrame) {
913     nsIContent* focusableContent = aFrame->GetContent();
914     MOZ_ASSERT(focusableContent, "Focusable frame must have content!");
915     RefPtr<Element> focusableElement = Element::FromNode(focusableContent);
916     fm->SetFocus(focusableElement, nsIFocusManager::FLAG_BYLONGPRESS);
917   } else {
918     nsPIDOMWindowOuter* win = mPresShell->GetDocument()->GetWindow();
919     if (win) {
920       fm->ClearFocus(win);
921       fm->SetFocusedWindow(win);
922     }
923   }
924 }
925 
SelectWord(nsIFrame * aFrame,const nsPoint & aPoint) const926 nsresult AccessibleCaretManager::SelectWord(nsIFrame* aFrame,
927                                             const nsPoint& aPoint) const {
928   AC_LOGV("%s", __FUNCTION__);
929 
930   SetSelectionDragState(true);
931   const RefPtr<nsPresContext> pinnedPresContext{mPresShell->GetPresContext()};
932   nsresult rs = aFrame->SelectByTypeAtPoint(pinnedPresContext, aPoint,
933                                             eSelectWord, eSelectWord, 0);
934 
935   SetSelectionDragState(false);
936   ClearMaintainedSelection();
937 
938   // Smart-select phone numbers if possible.
939   if (StaticPrefs::layout_accessiblecaret_extend_selection_for_phone_number()) {
940     SelectMoreIfPhoneNumber();
941   }
942 
943   return rs;
944 }
945 
SetSelectionDragState(bool aState) const946 void AccessibleCaretManager::SetSelectionDragState(bool aState) const {
947   RefPtr<nsFrameSelection> fs = GetFrameSelection();
948   if (fs) {
949     fs->SetDragState(aState);
950   }
951 }
952 
IsPhoneNumber(nsAString & aCandidate) const953 bool AccessibleCaretManager::IsPhoneNumber(nsAString& aCandidate) const {
954   RefPtr<Document> doc = mPresShell->GetDocument();
955   nsAutoString phoneNumberRegex(u"(^\\+)?[0-9 ,\\-.()*#pw]{1,30}$"_ns);
956   return nsContentUtils::IsPatternMatching(aCandidate, phoneNumberRegex, doc)
957       .valueOr(false);
958 }
959 
SelectMoreIfPhoneNumber() const960 void AccessibleCaretManager::SelectMoreIfPhoneNumber() const {
961   nsAutoString selectedText = StringifiedSelection();
962 
963   if (IsPhoneNumber(selectedText)) {
964     SetSelectionDirection(eDirNext);
965     ExtendPhoneNumberSelection(u"forward"_ns);
966 
967     SetSelectionDirection(eDirPrevious);
968     ExtendPhoneNumberSelection(u"backward"_ns);
969 
970     SetSelectionDirection(eDirNext);
971   }
972 }
973 
ExtendPhoneNumberSelection(const nsAString & aDirection) const974 void AccessibleCaretManager::ExtendPhoneNumberSelection(
975     const nsAString& aDirection) const {
976   if (!mPresShell) {
977     return;
978   }
979 
980   // Extend the phone number selection until we find a boundary.
981   RefPtr<Selection> selection = GetSelection();
982 
983   while (selection) {
984     const nsRange* anchorFocusRange = selection->GetAnchorFocusRange();
985     if (!anchorFocusRange) {
986       return;
987     }
988 
989     // Backup the anchor focus range since both anchor node and focus node might
990     // be changed after calling Selection::Modify().
991     RefPtr<nsRange> oldAnchorFocusRange = anchorFocusRange->CloneRange();
992 
993     // Save current focus node, focus offset and the selected text so that
994     // we can compare them with the modified ones later.
995     nsINode* oldFocusNode = selection->GetFocusNode();
996     uint32_t oldFocusOffset = selection->FocusOffset();
997     nsAutoString oldSelectedText = StringifiedSelection();
998 
999     // Extend the selection by one char.
1000     selection->Modify(u"extend"_ns, aDirection, u"character"_ns,
1001                       IgnoreErrors());
1002     if (IsTerminated() == Terminated::Yes) {
1003       return;
1004     }
1005 
1006     // If the selection didn't change, (can't extend further), we're done.
1007     if (selection->GetFocusNode() == oldFocusNode &&
1008         selection->FocusOffset() == oldFocusOffset) {
1009       return;
1010     }
1011 
1012     // If the changed selection isn't a valid phone number, we're done.
1013     // Also, if the selection was extended to a new block node, the string
1014     // returned by stringify() won't have a new line at the beginning or the
1015     // end of the string. Therefore, if either focus node or offset is
1016     // changed, but selected text is not changed, we're done, too.
1017     nsAutoString selectedText = StringifiedSelection();
1018 
1019     if (!IsPhoneNumber(selectedText) || oldSelectedText == selectedText) {
1020       // Backout the undesired selection extend, restore the old anchor focus
1021       // range before exit.
1022       selection->SetAnchorFocusToRange(oldAnchorFocusRange);
1023       return;
1024     }
1025   }
1026 }
1027 
SetSelectionDirection(nsDirection aDir) const1028 void AccessibleCaretManager::SetSelectionDirection(nsDirection aDir) const {
1029   Selection* selection = GetSelection();
1030   if (selection) {
1031     selection->AdjustAnchorFocusForMultiRange(aDir);
1032   }
1033 }
1034 
ClearMaintainedSelection() const1035 void AccessibleCaretManager::ClearMaintainedSelection() const {
1036   // Selection made by double-clicking for example will maintain the original
1037   // word selection. We should clear it so that we can drag caret freely.
1038   RefPtr<nsFrameSelection> fs = GetFrameSelection();
1039   if (fs) {
1040     fs->MaintainSelection(eSelectNoAmount);
1041   }
1042 }
1043 
MaybeFlush(const PresShell & aPresShell)1044 void AccessibleCaretManager::LayoutFlusher::MaybeFlush(
1045     const PresShell& aPresShell) {
1046   if (mAllowFlushing) {
1047     AutoRestore<bool> flushing(mFlushing);
1048     mFlushing = true;
1049 
1050     if (Document* doc = aPresShell.GetDocument()) {
1051       doc->FlushPendingNotifications(FlushType::Layout);
1052       // Don't access the PresShell after flushing, it could've become invalid.
1053     }
1054   }
1055 }
1056 
GetFrameForFirstRangeStartOrLastRangeEnd(nsDirection aDirection,int32_t * aOutOffset,nsIContent ** aOutContent,int32_t * aOutContentOffset) const1057 nsIFrame* AccessibleCaretManager::GetFrameForFirstRangeStartOrLastRangeEnd(
1058     nsDirection aDirection, int32_t* aOutOffset, nsIContent** aOutContent,
1059     int32_t* aOutContentOffset) const {
1060   if (!mPresShell) {
1061     return nullptr;
1062   }
1063 
1064   MOZ_ASSERT(GetCaretMode() == CaretMode::Selection);
1065   MOZ_ASSERT(aOutOffset, "aOutOffset shouldn't be nullptr!");
1066 
1067   const nsRange* range = nullptr;
1068   RefPtr<nsINode> startNode;
1069   RefPtr<nsINode> endNode;
1070   int32_t nodeOffset = 0;
1071   CaretAssociationHint hint;
1072 
1073   RefPtr<Selection> selection = GetSelection();
1074   bool findInFirstRangeStart = aDirection == eDirNext;
1075 
1076   if (findInFirstRangeStart) {
1077     range = selection->GetRangeAt(0);
1078     startNode = range->GetStartContainer();
1079     endNode = range->GetEndContainer();
1080     nodeOffset = range->StartOffset();
1081     hint = CARET_ASSOCIATE_AFTER;
1082   } else {
1083     MOZ_ASSERT(selection->RangeCount() > 0);
1084     range = selection->GetRangeAt(selection->RangeCount() - 1);
1085     startNode = range->GetEndContainer();
1086     endNode = range->GetStartContainer();
1087     nodeOffset = range->EndOffset();
1088     hint = CARET_ASSOCIATE_BEFORE;
1089   }
1090 
1091   nsCOMPtr<nsIContent> startContent = do_QueryInterface(startNode);
1092   nsIFrame* startFrame = nsFrameSelection::GetFrameForNodeOffset(
1093       startContent, nodeOffset, hint, aOutOffset);
1094 
1095   if (!startFrame) {
1096     ErrorResult err;
1097     RefPtr<TreeWalker> walker = mPresShell->GetDocument()->CreateTreeWalker(
1098         *startNode, dom::NodeFilter_Binding::SHOW_ALL, nullptr, err);
1099 
1100     if (!walker) {
1101       return nullptr;
1102     }
1103 
1104     startFrame = startContent ? startContent->GetPrimaryFrame() : nullptr;
1105     while (!startFrame && startNode != endNode) {
1106       startNode = findInFirstRangeStart ? walker->NextNode(err)
1107                                         : walker->PreviousNode(err);
1108 
1109       if (!startNode) {
1110         break;
1111       }
1112 
1113       startContent = startNode->AsContent();
1114       startFrame = startContent ? startContent->GetPrimaryFrame() : nullptr;
1115     }
1116 
1117     // We are walking among the nodes in the content tree, so the node offset
1118     // relative to startNode should be set to 0.
1119     nodeOffset = 0;
1120     *aOutOffset = 0;
1121   }
1122 
1123   if (startFrame) {
1124     if (aOutContent) {
1125       startContent.forget(aOutContent);
1126     }
1127     if (aOutContentOffset) {
1128       *aOutContentOffset = nodeOffset;
1129     }
1130   }
1131 
1132   return startFrame;
1133 }
1134 
RestrictCaretDraggingOffsets(nsIFrame::ContentOffsets & aOffsets)1135 bool AccessibleCaretManager::RestrictCaretDraggingOffsets(
1136     nsIFrame::ContentOffsets& aOffsets) {
1137   if (!mPresShell) {
1138     return false;
1139   }
1140 
1141   MOZ_ASSERT(GetCaretMode() == CaretMode::Selection);
1142 
1143   nsDirection dir =
1144       mActiveCaret == mCarets.GetFirst() ? eDirPrevious : eDirNext;
1145   int32_t offset = 0;
1146   nsCOMPtr<nsIContent> content;
1147   int32_t contentOffset = 0;
1148   nsIFrame* frame = GetFrameForFirstRangeStartOrLastRangeEnd(
1149       dir, &offset, getter_AddRefs(content), &contentOffset);
1150 
1151   if (!frame) {
1152     return false;
1153   }
1154 
1155   // Compare the active caret's new position (aOffsets) to the inactive caret's
1156   // position.
1157   NS_ASSERTION(contentOffset >= 0, "contentOffset should not be negative");
1158   const Maybe<int32_t> cmpToInactiveCaretPos =
1159       nsContentUtils::ComparePoints_AllowNegativeOffsets(
1160           aOffsets.content, aOffsets.StartOffset(), content, contentOffset);
1161   if (NS_WARN_IF(!cmpToInactiveCaretPos)) {
1162     // Potentially handle this properly when Selection across Shadow DOM
1163     // boundary is implemented
1164     // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1165     return false;
1166   }
1167 
1168   // Move one character (in the direction of dir) from the inactive caret's
1169   // position. This is the limit for the active caret's new position.
1170   nsPeekOffsetStruct limit(eSelectCluster, dir, offset, nsPoint(0, 0), true,
1171                            true, false, false, false);
1172   nsresult rv = frame->PeekOffset(&limit);
1173   if (NS_FAILED(rv)) {
1174     limit.mResultContent = content;
1175     limit.mContentOffset = contentOffset;
1176   }
1177 
1178   // Compare the active caret's new position (aOffsets) to the limit.
1179   NS_ASSERTION(limit.mContentOffset >= 0,
1180                "limit.mContentOffset should not be negative");
1181   const Maybe<int32_t> cmpToLimit =
1182       nsContentUtils::ComparePoints_AllowNegativeOffsets(
1183           aOffsets.content, aOffsets.StartOffset(), limit.mResultContent,
1184           limit.mContentOffset);
1185   if (NS_WARN_IF(!cmpToLimit)) {
1186     // Potentially handle this properly when Selection across Shadow DOM
1187     // boundary is implemented
1188     // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1189     return false;
1190   }
1191 
1192   auto SetOffsetsToLimit = [&aOffsets, &limit]() {
1193     aOffsets.content = limit.mResultContent;
1194     aOffsets.offset = limit.mContentOffset;
1195     aOffsets.secondaryOffset = limit.mContentOffset;
1196   };
1197 
1198   if (!StaticPrefs::
1199           layout_accessiblecaret_allow_dragging_across_other_caret()) {
1200     if ((mActiveCaret == mCarets.GetFirst() && *cmpToLimit == 1) ||
1201         (mActiveCaret == mCarets.GetSecond() && *cmpToLimit == -1)) {
1202       // The active caret's position is past the limit, which we don't allow
1203       // here. So set it to the limit, resulting in one character being
1204       // selected.
1205       SetOffsetsToLimit();
1206     }
1207   } else {
1208     switch (*cmpToInactiveCaretPos) {
1209       case 0:
1210         // The active caret's position is the same as the position of the
1211         // inactive caret. So set it to the limit to prevent the selection from
1212         // being collapsed, resulting in one character being selected.
1213         SetOffsetsToLimit();
1214         break;
1215       case 1:
1216         if (mActiveCaret == mCarets.GetFirst()) {
1217           // First caret was moved across the second caret. After making change
1218           // to the selection, the user will drag the second caret.
1219           mActiveCaret = mCarets.GetSecond();
1220         }
1221         break;
1222       case -1:
1223         if (mActiveCaret == mCarets.GetSecond()) {
1224           // Second caret was moved across the first caret. After making change
1225           // to the selection, the user will drag the first caret.
1226           mActiveCaret = mCarets.GetFirst();
1227         }
1228         break;
1229     }
1230   }
1231 
1232   return true;
1233 }
1234 
CompareTreePosition(nsIFrame * aStartFrame,nsIFrame * aEndFrame) const1235 bool AccessibleCaretManager::CompareTreePosition(nsIFrame* aStartFrame,
1236                                                  nsIFrame* aEndFrame) const {
1237   return (aStartFrame && aEndFrame &&
1238           nsLayoutUtils::CompareTreePosition(aStartFrame, aEndFrame) <= 0);
1239 }
1240 
DragCaretInternal(const nsPoint & aPoint)1241 nsresult AccessibleCaretManager::DragCaretInternal(const nsPoint& aPoint) {
1242   MOZ_ASSERT(mPresShell);
1243 
1244   nsIFrame* rootFrame = mPresShell->GetRootFrame();
1245   MOZ_ASSERT(rootFrame, "We need root frame to compute caret dragging!");
1246 
1247   nsPoint point = AdjustDragBoundary(
1248       nsPoint(aPoint.x, aPoint.y + mOffsetYToCaretLogicalPosition));
1249 
1250   // Find out which content we point to
1251 
1252   nsIFrame* ptFrame = nsLayoutUtils::GetFrameForPoint(
1253       RelativeTo{rootFrame}, point, GetHitTestOptions());
1254   if (!ptFrame) {
1255     return NS_ERROR_FAILURE;
1256   }
1257 
1258   RefPtr<nsFrameSelection> fs = GetFrameSelection();
1259   MOZ_ASSERT(fs);
1260 
1261   nsresult result;
1262   nsIFrame* newFrame = nullptr;
1263   nsPoint newPoint;
1264   nsPoint ptInFrame = point;
1265   nsLayoutUtils::TransformPoint(RelativeTo{rootFrame}, RelativeTo{ptFrame},
1266                                 ptInFrame);
1267   result = fs->ConstrainFrameAndPointToAnchorSubtree(ptFrame, ptInFrame,
1268                                                      &newFrame, newPoint);
1269   if (NS_FAILED(result) || !newFrame) {
1270     return NS_ERROR_FAILURE;
1271   }
1272 
1273   if (!newFrame->IsSelectable(nullptr)) {
1274     return NS_ERROR_FAILURE;
1275   }
1276 
1277   nsIFrame::ContentOffsets offsets =
1278       newFrame->GetContentOffsetsFromPoint(newPoint);
1279   if (offsets.IsNull()) {
1280     return NS_ERROR_FAILURE;
1281   }
1282 
1283   if (GetCaretMode() == CaretMode::Selection &&
1284       !RestrictCaretDraggingOffsets(offsets)) {
1285     return NS_ERROR_FAILURE;
1286   }
1287 
1288   ClearMaintainedSelection();
1289 
1290   const nsFrameSelection::FocusMode focusMode =
1291       (GetCaretMode() == CaretMode::Selection)
1292           ? nsFrameSelection::FocusMode::kExtendSelection
1293           : nsFrameSelection::FocusMode::kCollapseToNewPoint;
1294   fs->HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */,
1295                   offsets.StartOffset(), offsets.EndOffset(), focusMode,
1296                   offsets.associate);
1297   return NS_OK;
1298 }
1299 
1300 // static
GetAllChildFrameRectsUnion(nsIFrame * aFrame)1301 nsRect AccessibleCaretManager::GetAllChildFrameRectsUnion(nsIFrame* aFrame) {
1302   nsRect unionRect;
1303 
1304   // Drill through scroll frames, we don't want to include scrollbar child
1305   // frames below.
1306   for (nsIFrame* frame = aFrame->GetContentInsertionFrame(); frame;
1307        frame = frame->GetNextContinuation()) {
1308     nsRect frameRect;
1309 
1310     for (const auto& childList : frame->ChildLists()) {
1311       // Loop all children to union their scrollable overflow rect.
1312       for (nsIFrame* child : childList.mList) {
1313         nsRect childRect = child->ScrollableOverflowRectRelativeToSelf();
1314         nsLayoutUtils::TransformRect(child, frame, childRect);
1315 
1316         // A TextFrame containing only '\n' has positive height and width 0, or
1317         // positive width and height 0 if it's vertical. Need to use UnionEdges
1318         // to add its rect. BRFrame rect should be non-empty.
1319         if (childRect.IsEmpty()) {
1320           frameRect = frameRect.UnionEdges(childRect);
1321         } else {
1322           frameRect = frameRect.Union(childRect);
1323         }
1324       }
1325     }
1326 
1327     MOZ_ASSERT(!frameRect.IsEmpty(),
1328                "Editable frames should have at least one BRFrame child to make "
1329                "frameRect non-empty!");
1330     if (frame != aFrame) {
1331       nsLayoutUtils::TransformRect(frame, aFrame, frameRect);
1332     }
1333     unionRect = unionRect.Union(frameRect);
1334   }
1335 
1336   return unionRect;
1337 }
1338 
AdjustDragBoundary(const nsPoint & aPoint) const1339 nsPoint AccessibleCaretManager::AdjustDragBoundary(
1340     const nsPoint& aPoint) const {
1341   nsPoint adjustedPoint = aPoint;
1342 
1343   int32_t focusOffset = 0;
1344   nsIFrame* focusFrame =
1345       nsCaret::GetFrameAndOffset(GetSelection(), nullptr, 0, &focusOffset);
1346   Element* editingHost = GetEditingHostForFrame(focusFrame);
1347 
1348   if (editingHost) {
1349     nsIFrame* editingHostFrame = editingHost->GetPrimaryFrame();
1350     if (editingHostFrame) {
1351       nsRect boundary =
1352           AccessibleCaretManager::GetAllChildFrameRectsUnion(editingHostFrame);
1353       nsLayoutUtils::TransformRect(editingHostFrame, mPresShell->GetRootFrame(),
1354                                    boundary);
1355 
1356       // Shrink the rect to make sure we never hit the boundary.
1357       boundary.Deflate(kBoundaryAppUnits);
1358 
1359       adjustedPoint = boundary.ClampPoint(adjustedPoint);
1360     }
1361   }
1362 
1363   if (GetCaretMode() == CaretMode::Selection &&
1364       !StaticPrefs::
1365           layout_accessiblecaret_allow_dragging_across_other_caret()) {
1366     // Bug 1068474: Adjust the Y-coordinate so that the carets won't be in tilt
1367     // mode when a caret is being dragged surpass the other caret.
1368     //
1369     // For example, when dragging the second caret, the horizontal boundary
1370     // (lower bound) of its Y-coordinate is the logical position of the first
1371     // caret. Likewise, when dragging the first caret, the horizontal boundary
1372     // (upper bound) of its Y-coordinate is the logical position of the second
1373     // caret.
1374     if (mActiveCaret == mCarets.GetFirst()) {
1375       nscoord dragDownBoundaryY = mCarets.GetSecond()->LogicalPosition().y;
1376       if (dragDownBoundaryY > 0 && adjustedPoint.y > dragDownBoundaryY) {
1377         adjustedPoint.y = dragDownBoundaryY;
1378       }
1379     } else {
1380       nscoord dragUpBoundaryY = mCarets.GetFirst()->LogicalPosition().y;
1381       if (adjustedPoint.y < dragUpBoundaryY) {
1382         adjustedPoint.y = dragUpBoundaryY;
1383       }
1384     }
1385   }
1386 
1387   return adjustedPoint;
1388 }
1389 
StartSelectionAutoScrollTimer(const nsPoint & aPoint) const1390 void AccessibleCaretManager::StartSelectionAutoScrollTimer(
1391     const nsPoint& aPoint) const {
1392   Selection* selection = GetSelection();
1393   MOZ_ASSERT(selection);
1394 
1395   nsIFrame* anchorFrame = selection->GetPrimaryFrameForAnchorNode();
1396   if (!anchorFrame) {
1397     return;
1398   }
1399 
1400   nsIScrollableFrame* scrollFrame = nsLayoutUtils::GetNearestScrollableFrame(
1401       anchorFrame, nsLayoutUtils::SCROLLABLE_SAME_DOC |
1402                        nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
1403   if (!scrollFrame) {
1404     return;
1405   }
1406 
1407   nsIFrame* capturingFrame = scrollFrame->GetScrolledFrame();
1408   if (!capturingFrame) {
1409     return;
1410   }
1411 
1412   nsIFrame* rootFrame = mPresShell->GetRootFrame();
1413   MOZ_ASSERT(rootFrame);
1414   nsPoint ptInScrolled = aPoint;
1415   nsLayoutUtils::TransformPoint(RelativeTo{rootFrame},
1416                                 RelativeTo{capturingFrame}, ptInScrolled);
1417 
1418   RefPtr<nsFrameSelection> fs = GetFrameSelection();
1419   MOZ_ASSERT(fs);
1420   fs->StartAutoScrollTimer(capturingFrame, ptInScrolled, kAutoScrollTimerDelay);
1421 }
1422 
StopSelectionAutoScrollTimer() const1423 void AccessibleCaretManager::StopSelectionAutoScrollTimer() const {
1424   RefPtr<nsFrameSelection> fs = GetFrameSelection();
1425   MOZ_ASSERT(fs);
1426   fs->StopAutoScrollTimer();
1427 }
1428 
DispatchCaretStateChangedEvent(CaretChangedReason aReason)1429 void AccessibleCaretManager::DispatchCaretStateChangedEvent(
1430     CaretChangedReason aReason) {
1431   if (MaybeFlushLayout() == Terminated::Yes) {
1432     return;
1433   }
1434 
1435   const Selection* sel = GetSelection();
1436   if (!sel) {
1437     return;
1438   }
1439 
1440   Document* doc = mPresShell->GetDocument();
1441   MOZ_ASSERT(doc);
1442 
1443   CaretStateChangedEventInit init;
1444   init.mBubbles = true;
1445 
1446   const nsRange* range = sel->GetAnchorFocusRange();
1447   nsINode* commonAncestorNode = nullptr;
1448   if (range) {
1449     commonAncestorNode = range->GetClosestCommonInclusiveAncestor();
1450   }
1451 
1452   if (!commonAncestorNode) {
1453     commonAncestorNode = sel->GetFrameSelection()->GetAncestorLimiter();
1454   }
1455 
1456   RefPtr<DOMRect> domRect = new DOMRect(ToSupports(doc));
1457   nsRect rect = nsLayoutUtils::GetSelectionBoundingRect(sel);
1458 
1459   nsIFrame* commonAncestorFrame = nullptr;
1460   nsIFrame* rootFrame = mPresShell->GetRootFrame();
1461 
1462   if (commonAncestorNode && commonAncestorNode->IsContent()) {
1463     commonAncestorFrame = commonAncestorNode->AsContent()->GetPrimaryFrame();
1464   }
1465 
1466   if (commonAncestorFrame && rootFrame) {
1467     nsLayoutUtils::TransformRect(rootFrame, commonAncestorFrame, rect);
1468     nsRect clampedRect =
1469         nsLayoutUtils::ClampRectToScrollFrames(commonAncestorFrame, rect);
1470     nsLayoutUtils::TransformRect(commonAncestorFrame, rootFrame, clampedRect);
1471     rect = clampedRect;
1472     init.mSelectionVisible = !clampedRect.IsEmpty();
1473   } else {
1474     init.mSelectionVisible = true;
1475   }
1476 
1477   domRect->SetLayoutRect(rect);
1478 
1479   // Send isEditable info w/ event detail. This info can help determine
1480   // whether to show cut command on selection dialog or not.
1481   init.mSelectionEditable =
1482       commonAncestorFrame && GetEditingHostForFrame(commonAncestorFrame);
1483 
1484   init.mBoundingClientRect = domRect;
1485   init.mReason = aReason;
1486   init.mCollapsed = sel->IsCollapsed();
1487   init.mCaretVisible = mCarets.HasLogicallyVisibleCaret();
1488   init.mCaretVisuallyVisible = mCarets.HasVisuallyVisibleCaret();
1489   init.mSelectedTextContent = StringifiedSelection();
1490 
1491   RefPtr<CaretStateChangedEvent> event = CaretStateChangedEvent::Constructor(
1492       doc, u"mozcaretstatechanged"_ns, init);
1493 
1494   event->SetTrusted(true);
1495   event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true;
1496 
1497   AC_LOG("%s: reason %" PRIu32 ", collapsed %d, caretVisible %" PRIu32,
1498          __FUNCTION__, static_cast<uint32_t>(init.mReason), init.mCollapsed,
1499          static_cast<uint32_t>(init.mCaretVisible));
1500 
1501   (new AsyncEventDispatcher(doc, event))->PostDOMEvent();
1502 }
1503 
Carets(UniquePtr<AccessibleCaret> aFirst,UniquePtr<AccessibleCaret> aSecond)1504 AccessibleCaretManager::Carets::Carets(UniquePtr<AccessibleCaret> aFirst,
1505                                        UniquePtr<AccessibleCaret> aSecond)
1506     : mFirst{std::move(aFirst)}, mSecond{std::move(aSecond)} {}
1507 
1508 }  // namespace mozilla
1509