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