summaryrefslogtreecommitdiffstats
path: root/layout/base/AccessibleCaretManager.h
blob: 036151d68a2d9c697080aa1c7df3b1880292dd75 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#ifndef AccessibleCaretManager_h
#define AccessibleCaretManager_h

#include "AccessibleCaret.h"

#include "mozilla/Attributes.h"
#include "mozilla/dom/CaretStateChangedEvent.h"
#include "mozilla/dom/MouseEventBinding.h"
#include "mozilla/EnumSet.h"
#include "mozilla/EventForwards.h"
#include "mozilla/RefPtr.h"
#include "mozilla/UniquePtr.h"
#include "nsCOMPtr.h"
#include "nsCoord.h"
#include "nsIFrame.h"
#include "nsISelectionListener.h"

class nsFrameSelection;
class nsIContent;

struct nsPoint;

namespace mozilla {
class PresShell;
namespace dom {
class Element;
class Selection;
}  // namespace dom

// -----------------------------------------------------------------------------
// AccessibleCaretManager does not deal with events or callbacks directly. It
// relies on AccessibleCaretEventHub to call its public methods to do the work.
// All codes needed to interact with PresShell, Selection, and AccessibleCaret
// should be written in AccessibleCaretManager.
//
// None the public methods in AccessibleCaretManager will flush layout or style
// prior to performing its task. The caller must ensure the layout is up to
// date.
// TODO: it's unclear, whether that's true. `OnSelectionChanged` calls
// `UpdateCarets`, which may flush layout.
//
// Please see the wiki page for more information.
// https://wiki.mozilla.org/AccessibleCaret
//
class AccessibleCaretManager {
 public:
  // @param aPresShell may be nullptr for testing.
  explicit AccessibleCaretManager(PresShell* aPresShell);
  virtual ~AccessibleCaretManager() = default;

  // Called by AccessibleCaretEventHub to inform us that PresShell is destroyed.
  void Terminate();

  // The aPoint in the following public methods should be relative to root
  // frame.

  // Press caret on the given point. Return NS_OK if the point is actually on
  // one of the carets.
  MOZ_CAN_RUN_SCRIPT
  virtual nsresult PressCaret(const nsPoint& aPoint, EventClassID aEventClass);

  // Drag caret to the given point. It's required to call PressCaret()
  // beforehand.
  MOZ_CAN_RUN_SCRIPT
  virtual nsresult DragCaret(const nsPoint& aPoint);

  // Release caret from he previous press action. It's required to call
  // PressCaret() beforehand.
  MOZ_CAN_RUN_SCRIPT
  virtual nsresult ReleaseCaret();

  // A quick single tap on caret on given point without dragging.
  MOZ_CAN_RUN_SCRIPT
  virtual nsresult TapCaret(const nsPoint& aPoint);

  // Select a word or bring up paste shortcut (if Gaia is listening) under the
  // given point.
  MOZ_CAN_RUN_SCRIPT
  virtual nsresult SelectWordOrShortcut(const nsPoint& aPoint);

  // Handle scroll-start event.
  MOZ_CAN_RUN_SCRIPT
  virtual void OnScrollStart();

  // Handle scroll-end event.
  MOZ_CAN_RUN_SCRIPT
  virtual void OnScrollEnd();

  // Handle ScrollPositionChanged from nsIScrollObserver. This might be called
  // at anytime, not necessary between OnScrollStart and OnScrollEnd.
  MOZ_CAN_RUN_SCRIPT
  virtual void OnScrollPositionChanged();

  // Handle reflow event from nsIReflowObserver.
  MOZ_CAN_RUN_SCRIPT
  virtual void OnReflow();

  // Handle blur event from nsFocusManager.
  MOZ_CAN_RUN_SCRIPT
  virtual void OnBlur();

  // Handle NotifySelectionChanged event from nsISelectionListener.
  // @param aReason potentially multiple of the reasons defined in
  //                nsISelectionListener.idl.
  MOZ_CAN_RUN_SCRIPT
  virtual nsresult OnSelectionChanged(dom::Document* aDoc, dom::Selection* aSel,
                                      int16_t aReason);
  // Handle key event.
  MOZ_CAN_RUN_SCRIPT
  virtual void OnKeyboardEvent();

  // Update the manager with the last input source that was observed. This
  // is used in part to determine if the carets should be shown or hidden.
  void SetLastInputSource(uint16_t aInputSource);

  // Returns True indicating that we should disable APZ to avoid jumpy carets.
  bool ShouldDisableApz() const;

 protected:
  class Carets;

  // @param aPresShell may be nullptr for testing.
  AccessibleCaretManager(PresShell* aPresShell, Carets aCarets);

  // This enum representing the number of AccessibleCarets on the screen.
  enum class CaretMode : uint8_t {
    // No caret on the screen.
    None,

    // One caret, i.e. the selection is collapsed.
    Cursor,

    // Two carets, i.e. the selection is not collapsed.
    Selection
  };

  friend std::ostream& operator<<(std::ostream& aStream,
                                  const CaretMode& aCaretMode);

  enum class UpdateCaretsHint : uint8_t {
    // Update everything including appearance and position.
    Default,

    // Update everything while respecting the old appearance. For example, if
    // the caret in cursor mode is hidden due to blur, do not change its
    // appearance to Normal.
    RespectOldAppearance,

    // No CaretStateChangedEvent will be dispatched in the end of
    // UpdateCarets().
    DispatchNoEvent,
  };

  using UpdateCaretsHintSet = mozilla::EnumSet<UpdateCaretsHint>;

  friend std::ostream& operator<<(std::ostream& aStream,
                                  const UpdateCaretsHint& aResult);

  enum class Terminated : bool { No, Yes };

  // This method could kill the shell, so callers to methods that call
  // MaybeFlushLayout should ensure the event hub that owns us is still alive.
  //
  // See the mRefCnt assertions in AccessibleCaretEventHub.
  //
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT virtual Terminated MaybeFlushLayout();

  // Update carets based on current selection status. This function will flush
  // layout, so caller must ensure the PresShell is still valid after calling
  // this method.
  MOZ_CAN_RUN_SCRIPT
  void UpdateCarets(
      const UpdateCaretsHintSet& aHints = UpdateCaretsHint::Default);

  // Force hiding all carets regardless of the current selection status, and
  // dispatch CaretStateChangedEvent if one of the carets is logically-visible.
  MOZ_CAN_RUN_SCRIPT
  void HideCaretsAndDispatchCaretStateChangedEvent();

  MOZ_CAN_RUN_SCRIPT
  void UpdateCaretsForCursorMode(const UpdateCaretsHintSet& aHints);

  MOZ_CAN_RUN_SCRIPT
  void UpdateCaretsForSelectionMode(const UpdateCaretsHintSet& aHints);

  // A helper function to update mShouldDisableApz.
  void UpdateShouldDisableApz();

  // Provide haptic / touch feedback, primarily for select on longpress.
  void ProvideHapticFeedback();

  // Get the nearest enclosing focusable frame of aFrame.
  // @return focusable frame if there is any; nullptr otherwise.
  nsIFrame* GetFocusableFrame(nsIFrame* aFrame) const;

  // Change focus to aFrame if it isn't nullptr. Otherwise, clear the old focus
  // then re-focus the window.
  MOZ_CAN_RUN_SCRIPT_BOUNDARY void ChangeFocusToOrClearOldFocus(
      nsIFrame* aFrame) const;

  MOZ_CAN_RUN_SCRIPT
  nsresult SelectWord(nsIFrame* aFrame, const nsPoint& aPoint) const;
  MOZ_CAN_RUN_SCRIPT void SetSelectionDragState(bool aState) const;

  // Return true if the candidate string is a phone number.
  bool IsPhoneNumber(const nsAString& aCandidate) const;

  // Extend the current selection forwards and backwards if it's already a
  // phone number.
  MOZ_CAN_RUN_SCRIPT
  void SelectMoreIfPhoneNumber() const;

  // Extend the current phone number selection in the requested direction.
  MOZ_CAN_RUN_SCRIPT
  void ExtendPhoneNumberSelection(const nsAString& aDirection) const;

  void SetSelectionDirection(nsDirection aDir) const;

  // If aDirection is eDirNext, get the frame for the range start in the first
  // range from the current selection, and return the offset into that frame as
  // well as the range start content and the content offset. Otherwise, get the
  // frame and the offset for the range end in the last range instead.
  nsIFrame* GetFrameForFirstRangeStartOrLastRangeEnd(
      nsDirection aDirection, int32_t* aOutOffset,
      nsIContent** aOutContent = nullptr,
      int32_t* aOutContentOffset = nullptr) const;

  MOZ_CAN_RUN_SCRIPT nsresult DragCaretInternal(const nsPoint& aPoint);
  nsPoint AdjustDragBoundary(const nsPoint& aPoint) const;

  // Start the selection scroll timer if the caret is being dragged out of
  // the scroll port.
  MOZ_CAN_RUN_SCRIPT
  void StartSelectionAutoScrollTimer(const nsPoint& aPoint) const;
  void StopSelectionAutoScrollTimer() const;

  void ClearMaintainedSelection() const;

  static dom::Element* GetEditingHostForFrame(const nsIFrame* aFrame);
  dom::Selection* GetSelection() const;
  already_AddRefed<nsFrameSelection> GetFrameSelection() const;

  MOZ_CAN_RUN_SCRIPT
  nsAutoString StringifiedSelection() const;

  // Get the union of all the child frame scrollable overflow rects for aFrame,
  // which is used as a helper function to restrict the area where the caret can
  // be dragged. Returns the rect relative to aFrame.
  static nsRect GetAllChildFrameRectsUnion(nsIFrame* aFrame);

  // Restrict the active caret's dragging position based on
  // sCaretsAllowDraggingAcrossOtherCaret. If the active caret is the first
  // caret, the `limit` will be the previous character of the second caret.
  // Otherwise, the `limit` will be the next character of the first caret.
  //
  // @param aOffsets is the new position of the active caret, and it will be set
  // to the `limit` when 1) sCaretsAllowDraggingAcrossOtherCaret is false and
  // it's being dragged past the limit. 2) sCaretsAllowDraggingAcrossOtherCaret
  // is true and the active caret's position is the same as the inactive's
  // position.
  // @return true if the aOffsets is suitable for changing the selection.
  bool RestrictCaretDraggingOffsets(nsIFrame::ContentOffsets& aOffsets);

  // ---------------------------------------------------------------------------
  // The following functions are made virtual for stubbing or mocking in gtest.
  //
  // @return Yes if Terminate() had been called.
  virtual Terminated IsTerminated() const {
    return mPresShell ? Terminated::No : Terminated::Yes;
  }

  // Get caret mode based on current selection.
  virtual CaretMode GetCaretMode() const;

  // @return true if aStartFrame comes before aEndFrame.
  virtual bool CompareTreePosition(nsIFrame* aStartFrame,
                                   nsIFrame* aEndFrame) const;

  // Check if the two carets is overlapping to become tilt.
  // @return true if the two carets become tilt; false, otherwise.
  virtual bool UpdateCaretsForOverlappingTilt();

  // Make the two carets always tilt.
  virtual void UpdateCaretsForAlwaysTilt(const nsIFrame* aStartFrame,
                                         const nsIFrame* aEndFrame);

  // Check whether AccessibleCaret is displayable in cursor mode or not.
  // @param aOutFrame returns frame of the cursor if it's displayable.
  // @param aOutOffset returns frame offset as well.
  virtual bool IsCaretDisplayableInCursorMode(
      nsIFrame** aOutFrame = nullptr, int32_t* aOutOffset = nullptr) const;

  virtual bool HasNonEmptyTextContent(nsINode* aNode) const;

  // This function will flush layout, so caller must ensure the PresShell is
  // still valid after calling this method.
  // @param aPoint The event point when the user is pressing or dragging a
  //               caret, which is relative to the root frame.
  MOZ_CAN_RUN_SCRIPT
  virtual void DispatchCaretStateChangedEvent(dom::CaretChangedReason aReason,
                                              const nsPoint* aPoint = nullptr);

  // ---------------------------------------------------------------------------
  // Member variables
  //
  nscoord mOffsetYToCaretLogicalPosition = NS_UNCONSTRAINEDSIZE;

  // AccessibleCaretEventHub owns us by a UniquePtr. When it's destroyed, we'll
  // also be destroyed. No need to worry if we outlive mPresShell.
  //
  // mPresShell will be set to nullptr in Terminate(). Therefore mPresShell is
  // nullptr either we are in gtest or PresShell::IsDestroying() is true.
  PresShell* MOZ_NON_OWNING_REF mPresShell = nullptr;

  class Carets {
   public:
    Carets(UniquePtr<AccessibleCaret> aFirst,
           UniquePtr<AccessibleCaret> aSecond);

    Carets(Carets&&) = default;
    Carets(const Carets&) = delete;
    Carets& operator=(const Carets&) = delete;

    AccessibleCaret* GetFirst() const { return mFirst.get(); }

    AccessibleCaret* GetSecond() const { return mSecond.get(); }

    bool HasLogicallyVisibleCaret() const {
      return mFirst->IsLogicallyVisible() || mSecond->IsLogicallyVisible();
    }

    bool HasVisuallyVisibleCaret() const {
      return mFirst->IsVisuallyVisible() || mSecond->IsVisuallyVisible();
    }

    void Terminate() {
      mFirst = nullptr;
      mSecond = nullptr;
    }

   private:
    // First caret is attached to nsCaret in cursor mode, and is attached to
    // selection highlight as the left caret in selection mode.
    UniquePtr<AccessibleCaret> mFirst;

    // Second caret is used solely in selection mode, and is attached to
    // selection highlight as the right caret.
    UniquePtr<AccessibleCaret> mSecond;
  };

  Carets mCarets;

  // The caret being pressed or dragged.
  AccessibleCaret* mActiveCaret = nullptr;

  // The caret mode since last update carets.
  CaretMode mLastUpdateCaretMode = CaretMode::None;

  // The last input source that the event hub saw. We use this to decide whether
  // or not show the carets when the selection is updated, as we want to hide
  // the carets for mouse-triggered selection changes but show them for other
  // input types such as touch.
  uint16_t mLastInputSource = dom::MouseEvent_Binding::MOZ_SOURCE_UNKNOWN;

  // Set to true in OnScrollStart() and set to false in OnScrollEnd().
  bool mIsScrollStarted = false;

  class LayoutFlusher final {
   public:
    LayoutFlusher() = default;

    ~LayoutFlusher();

    LayoutFlusher(const LayoutFlusher&) = delete;
    LayoutFlusher& operator=(const LayoutFlusher&) = delete;

    MOZ_CAN_RUN_SCRIPT void MaybeFlush(const PresShell& aPresShell);

    // Set to false to disallow flushing layout in some callbacks such as
    // OnReflow(), OnScrollStart(), OnScrollStart(), or
    // OnScrollPositionChanged().
    bool mAllowFlushing = true;

   private:
    // Whether we're flushing layout, used for sanity-checking.
    bool mFlushing = false;
  };

  LayoutFlusher mLayoutFlusher;

  // Set to True if one of the caret's position is changed in last update.
  bool mIsCaretPositionChanged = false;

  class DesiredAsyncPanZoomState final {
   public:
    void Update(const AccessibleCaretManager& aAccessibleCaretManager);

    enum class Value : bool { Disabled, Enabled };

    Value Get() const { return mValue; }

   private:
    Value mValue = Value::Enabled;
  };

  DesiredAsyncPanZoomState mDesiredAsyncPanZoomState;

  static const int32_t kAutoScrollTimerDelay = 30;

  // Clicking on the boundary of input or textarea will move the caret to the
  // front or end of the content. To avoid this, we need to deflate the content
  // boundary by 61 app units, which is 1 pixel + 1 app unit as defined in
  // AppUnit.h.
  static const int32_t kBoundaryAppUnits = 61;

  enum ScriptUpdateMode : int32_t {
    // By default, always hide carets for selection changes due to JS calls.
    kScriptAlwaysHide,
    // Update any visible carets for selection changes due to JS calls,
    // but don't show carets if carets are hidden.
    kScriptUpdateVisible,
    // Always show carets for selection changes due to JS calls.
    kScriptAlwaysShow
  };
};

std::ostream& operator<<(std::ostream& aStream,
                         const AccessibleCaretManager::CaretMode& aCaretMode);

std::ostream& operator<<(
    std::ostream& aStream,
    const AccessibleCaretManager::UpdateCaretsHint& aResult);

}  // namespace mozilla

#endif  // AccessibleCaretManager_h