Coverage Report

Created: 2018-09-25 14:53

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