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