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