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 #ifndef AccessibleCaretManager_h
8 #define AccessibleCaretManager_h
9 
10 #include "AccessibleCaret.h"
11 
12 #include "mozilla/Attributes.h"
13 #include "mozilla/dom/CaretStateChangedEvent.h"
14 #include "mozilla/dom/MouseEventBinding.h"
15 #include "mozilla/EnumSet.h"
16 #include "mozilla/EventForwards.h"
17 #include "mozilla/RefPtr.h"
18 #include "mozilla/UniquePtr.h"
19 #include "nsCOMPtr.h"
20 #include "nsCoord.h"
21 #include "nsIFrame.h"
22 #include "nsISelectionListener.h"
23 
24 class nsFrameSelection;
25 class nsIContent;
26 
27 struct nsPoint;
28 
29 namespace mozilla {
30 class PresShell;
31 namespace dom {
32 class Element;
33 class Selection;
34 }  // namespace dom
35 
36 // -----------------------------------------------------------------------------
37 // AccessibleCaretManager does not deal with events or callbacks directly. It
38 // relies on AccessibleCaretEventHub to call its public methods to do the work.
39 // All codes needed to interact with PresShell, Selection, and AccessibleCaret
40 // should be written in AccessibleCaretManager.
41 //
42 // None the public methods in AccessibleCaretManager will flush layout or style
43 // prior to performing its task. The caller must ensure the layout is up to
44 // date.
45 // TODO: it's unclear, whether that's true. `OnSelectionChanged` calls
46 // `UpdateCarets`, which may flush layout.
47 //
48 // Please see the wiki page for more information.
49 // https://wiki.mozilla.org/AccessibleCaret
50 //
51 class AccessibleCaretManager {
52  public:
53   // @param aPresShell may be nullptr for testing.
54   explicit AccessibleCaretManager(PresShell* aPresShell);
55   virtual ~AccessibleCaretManager() = default;
56 
57   // Called by AccessibleCaretEventHub to inform us that PresShell is destroyed.
58   void Terminate();
59 
60   // The aPoint in the following public methods should be relative to root
61   // frame.
62 
63   // Press caret on the given point. Return NS_OK if the point is actually on
64   // one of the carets.
65   MOZ_CAN_RUN_SCRIPT
66   virtual nsresult PressCaret(const nsPoint& aPoint, EventClassID aEventClass);
67 
68   // Drag caret to the given point. It's required to call PressCaret()
69   // beforehand.
70   MOZ_CAN_RUN_SCRIPT
71   virtual nsresult DragCaret(const nsPoint& aPoint);
72 
73   // Release caret from he previous press action. It's required to call
74   // PressCaret() beforehand.
75   MOZ_CAN_RUN_SCRIPT
76   virtual nsresult ReleaseCaret();
77 
78   // A quick single tap on caret on given point without dragging.
79   MOZ_CAN_RUN_SCRIPT
80   virtual nsresult TapCaret(const nsPoint& aPoint);
81 
82   // Select a word or bring up paste shortcut (if Gaia is listening) under the
83   // given point.
84   MOZ_CAN_RUN_SCRIPT
85   virtual nsresult SelectWordOrShortcut(const nsPoint& aPoint);
86 
87   // Handle scroll-start event.
88   MOZ_CAN_RUN_SCRIPT
89   virtual void OnScrollStart();
90 
91   // Handle scroll-end event.
92   MOZ_CAN_RUN_SCRIPT
93   virtual void OnScrollEnd();
94 
95   // Handle ScrollPositionChanged from nsIScrollObserver. This might be called
96   // at anytime, not necessary between OnScrollStart and OnScrollEnd.
97   MOZ_CAN_RUN_SCRIPT
98   virtual void OnScrollPositionChanged();
99 
100   // Handle reflow event from nsIReflowObserver.
101   MOZ_CAN_RUN_SCRIPT
102   virtual void OnReflow();
103 
104   // Handle blur event from nsFocusManager.
105   MOZ_CAN_RUN_SCRIPT
106   virtual void OnBlur();
107 
108   // Handle NotifySelectionChanged event from nsISelectionListener.
109   // @param aReason potentially multiple of the reasons defined in
110   //                nsISelectionListener.idl.
111   MOZ_CAN_RUN_SCRIPT
112   virtual nsresult OnSelectionChanged(dom::Document* aDoc, dom::Selection* aSel,
113                                       int16_t aReason);
114   // Handle key event.
115   MOZ_CAN_RUN_SCRIPT
116   virtual void OnKeyboardEvent();
117 
118   // The canvas frame holding the accessible caret anonymous content elements
119   // was reconstructed, resulting in the content elements getting cloned.
120   virtual void OnFrameReconstruction();
121 
122   // Update the manager with the last input source that was observed. This
123   // is used in part to determine if the carets should be shown or hidden.
124   void SetLastInputSource(uint16_t aInputSource);
125 
126   // Returns True indicating that we should disable APZ to avoid jumpy carets.
127   bool ShouldDisableApz() const;
128 
129  protected:
130   class Carets;
131 
132   // @param aPresShell may be nullptr for testing.
133   AccessibleCaretManager(PresShell* aPresShell, Carets aCarets);
134 
135   // This enum representing the number of AccessibleCarets on the screen.
136   enum class CaretMode : uint8_t {
137     // No caret on the screen.
138     None,
139 
140     // One caret, i.e. the selection is collapsed.
141     Cursor,
142 
143     // Two carets, i.e. the selection is not collapsed.
144     Selection
145   };
146 
147   friend std::ostream& operator<<(std::ostream& aStream,
148                                   const CaretMode& aCaretMode);
149 
150   enum class UpdateCaretsHint : uint8_t {
151     // Update everything including appearance and position.
152     Default,
153 
154     // Update everything while respecting the old appearance. For example, if
155     // the caret in cursor mode is hidden due to blur, do not change its
156     // appearance to Normal.
157     RespectOldAppearance,
158 
159     // No CaretStateChangedEvent will be dispatched in the end of
160     // UpdateCarets().
161     DispatchNoEvent,
162   };
163 
164   using UpdateCaretsHintSet = mozilla::EnumSet<UpdateCaretsHint>;
165 
166   friend std::ostream& operator<<(std::ostream& aStream,
167                                   const UpdateCaretsHint& aResult);
168 
169   enum class Terminated : bool { No, Yes };
170 
171   // This method could kill the shell, so callers to methods that call
172   // MaybeFlushLayout should ensure the event hub that owns us is still alive.
173   //
174   // See the mRefCnt assertions in AccessibleCaretEventHub.
175   //
176   [[nodiscard]] MOZ_CAN_RUN_SCRIPT virtual Terminated MaybeFlushLayout();
177 
178   // Update carets based on current selection status. This function will flush
179   // layout, so caller must ensure the PresShell is still valid after calling
180   // this method.
181   MOZ_CAN_RUN_SCRIPT
182   void UpdateCarets(
183       const UpdateCaretsHintSet& aHints = UpdateCaretsHint::Default);
184 
185   // Force hiding all carets regardless of the current selection status, and
186   // dispatch CaretStateChangedEvent if one of the carets is logically-visible.
187   MOZ_CAN_RUN_SCRIPT
188   void HideCaretsAndDispatchCaretStateChangedEvent();
189 
190   MOZ_CAN_RUN_SCRIPT
191   void UpdateCaretsForCursorMode(const UpdateCaretsHintSet& aHints);
192 
193   MOZ_CAN_RUN_SCRIPT
194   void UpdateCaretsForSelectionMode(const UpdateCaretsHintSet& aHints);
195 
196   // A helper function to update mShouldDisableApz.
197   void UpdateShouldDisableApz();
198 
199   // Provide haptic / touch feedback, primarily for select on longpress.
200   void ProvideHapticFeedback();
201 
202   // Get the nearest enclosing focusable frame of aFrame.
203   // @return focusable frame if there is any; nullptr otherwise.
204   nsIFrame* GetFocusableFrame(nsIFrame* aFrame) const;
205 
206   // Change focus to aFrame if it isn't nullptr. Otherwise, clear the old focus
207   // then re-focus the window.
208   MOZ_CAN_RUN_SCRIPT_BOUNDARY void ChangeFocusToOrClearOldFocus(
209       nsIFrame* aFrame) const;
210 
211   MOZ_CAN_RUN_SCRIPT
212   nsresult SelectWord(nsIFrame* aFrame, const nsPoint& aPoint) const;
213   MOZ_CAN_RUN_SCRIPT void SetSelectionDragState(bool aState) const;
214 
215   // Return true if the candidate string is a phone number.
216   bool IsPhoneNumber(nsAString& aCandidate) const;
217 
218   // Extend the current selection forwards and backwards if it's already a
219   // phone number.
220   MOZ_CAN_RUN_SCRIPT
221   void SelectMoreIfPhoneNumber() const;
222 
223   // Extend the current phone number selection in the requested direction.
224   MOZ_CAN_RUN_SCRIPT
225   void ExtendPhoneNumberSelection(const nsAString& aDirection) const;
226 
227   void SetSelectionDirection(nsDirection aDir) const;
228 
229   // If aDirection is eDirNext, get the frame for the range start in the first
230   // range from the current selection, and return the offset into that frame as
231   // well as the range start content and the content offset. Otherwise, get the
232   // frame and the offset for the range end in the last range instead.
233   nsIFrame* GetFrameForFirstRangeStartOrLastRangeEnd(
234       nsDirection aDirection, int32_t* aOutOffset,
235       nsIContent** aOutContent = nullptr,
236       int32_t* aOutContentOffset = nullptr) const;
237 
238   MOZ_CAN_RUN_SCRIPT nsresult DragCaretInternal(const nsPoint& aPoint);
239   nsPoint AdjustDragBoundary(const nsPoint& aPoint) const;
240 
241   // Start the selection scroll timer if the caret is being dragged out of
242   // the scroll port.
243   MOZ_CAN_RUN_SCRIPT
244   void StartSelectionAutoScrollTimer(const nsPoint& aPoint) const;
245   void StopSelectionAutoScrollTimer() const;
246 
247   void ClearMaintainedSelection() const;
248 
249   static dom::Element* GetEditingHostForFrame(const nsIFrame* aFrame);
250   dom::Selection* GetSelection() const;
251   already_AddRefed<nsFrameSelection> GetFrameSelection() const;
252 
253   MOZ_CAN_RUN_SCRIPT
254   nsAutoString StringifiedSelection() const;
255 
256   // Get the union of all the child frame scrollable overflow rects for aFrame,
257   // which is used as a helper function to restrict the area where the caret can
258   // be dragged. Returns the rect relative to aFrame.
259   static nsRect GetAllChildFrameRectsUnion(nsIFrame* aFrame);
260 
261   // Restrict the active caret's dragging position based on
262   // sCaretsAllowDraggingAcrossOtherCaret. If the active caret is the first
263   // caret, the `limit` will be the previous character of the second caret.
264   // Otherwise, the `limit` will be the next character of the first caret.
265   //
266   // @param aOffsets is the new position of the active caret, and it will be set
267   // to the `limit` when 1) sCaretsAllowDraggingAcrossOtherCaret is false and
268   // it's being dragged past the limit. 2) sCaretsAllowDraggingAcrossOtherCaret
269   // is true and the active caret's position is the same as the inactive's
270   // position.
271   // @return true if the aOffsets is suitable for changing the selection.
272   bool RestrictCaretDraggingOffsets(nsIFrame::ContentOffsets& aOffsets);
273 
274   // ---------------------------------------------------------------------------
275   // The following functions are made virtual for stubbing or mocking in gtest.
276   //
277   // @return Yes if Terminate() had been called.
IsTerminated()278   virtual Terminated IsTerminated() const {
279     return mPresShell ? Terminated::No : Terminated::Yes;
280   }
281 
282   // Get caret mode based on current selection.
283   virtual CaretMode GetCaretMode() const;
284 
285   // @return true if aStartFrame comes before aEndFrame.
286   virtual bool CompareTreePosition(nsIFrame* aStartFrame,
287                                    nsIFrame* aEndFrame) const;
288 
289   // Check if the two carets is overlapping to become tilt.
290   // @return true if the two carets become tilt; false, otherwise.
291   virtual bool UpdateCaretsForOverlappingTilt();
292 
293   // Make the two carets always tilt.
294   virtual void UpdateCaretsForAlwaysTilt(const nsIFrame* aStartFrame,
295                                          const nsIFrame* aEndFrame);
296 
297   // Check whether AccessibleCaret is displayable in cursor mode or not.
298   // @param aOutFrame returns frame of the cursor if it's displayable.
299   // @param aOutOffset returns frame offset as well.
300   virtual bool IsCaretDisplayableInCursorMode(
301       nsIFrame** aOutFrame = nullptr, int32_t* aOutOffset = nullptr) const;
302 
303   virtual bool HasNonEmptyTextContent(nsINode* aNode) const;
304 
305   // This function will flush layout, so caller must ensure the PresShell is
306   // still valid after calling this method.
307   MOZ_CAN_RUN_SCRIPT
308   virtual void DispatchCaretStateChangedEvent(dom::CaretChangedReason aReason);
309 
310   // ---------------------------------------------------------------------------
311   // Member variables
312   //
313   nscoord mOffsetYToCaretLogicalPosition = NS_UNCONSTRAINEDSIZE;
314 
315   // AccessibleCaretEventHub owns us by a UniquePtr. When it's destroyed, we'll
316   // also be destroyed. No need to worry if we outlive mPresShell.
317   //
318   // mPresShell will be set to nullptr in Terminate(). Therefore mPresShell is
319   // nullptr either we are in gtest or PresShell::IsDestroying() is true.
320   PresShell* MOZ_NON_OWNING_REF mPresShell = nullptr;
321 
322   class Carets {
323    public:
324     Carets(UniquePtr<AccessibleCaret> aFirst,
325            UniquePtr<AccessibleCaret> aSecond);
326 
327     Carets(Carets&&) = default;
328     Carets(const Carets&) = delete;
329     Carets& operator=(const Carets&) = delete;
330 
GetFirst()331     AccessibleCaret* GetFirst() const { return mFirst.get(); }
332 
GetSecond()333     AccessibleCaret* GetSecond() const { return mSecond.get(); }
334 
HasLogicallyVisibleCaret()335     bool HasLogicallyVisibleCaret() const {
336       return mFirst->IsLogicallyVisible() || mSecond->IsLogicallyVisible();
337     }
338 
HasVisuallyVisibleCaret()339     bool HasVisuallyVisibleCaret() const {
340       return mFirst->IsVisuallyVisible() || mSecond->IsVisuallyVisible();
341     }
342 
Terminate()343     void Terminate() {
344       mFirst = nullptr;
345       mSecond = nullptr;
346     }
347 
348    private:
349     // First caret is attached to nsCaret in cursor mode, and is attached to
350     // selection highlight as the left caret in selection mode.
351     UniquePtr<AccessibleCaret> mFirst;
352 
353     // Second caret is used solely in selection mode, and is attached to
354     // selection highlight as the right caret.
355     UniquePtr<AccessibleCaret> mSecond;
356   };
357 
358   Carets mCarets;
359 
360   // The caret being pressed or dragged.
361   AccessibleCaret* mActiveCaret = nullptr;
362 
363   // The caret mode since last update carets.
364   CaretMode mLastUpdateCaretMode = CaretMode::None;
365 
366   // The last input source that the event hub saw. We use this to decide whether
367   // or not show the carets when the selection is updated, as we want to hide
368   // the carets for mouse-triggered selection changes but show them for other
369   // input types such as touch.
370   uint16_t mLastInputSource = dom::MouseEvent_Binding::MOZ_SOURCE_UNKNOWN;
371 
372   // Set to true in OnScrollStart() and set to false in OnScrollEnd().
373   bool mIsScrollStarted = false;
374 
375   class LayoutFlusher final {
376    public:
377     LayoutFlusher() = default;
378 
379     ~LayoutFlusher();
380 
381     LayoutFlusher(const LayoutFlusher&) = delete;
382     LayoutFlusher& operator=(const LayoutFlusher&) = delete;
383 
384     MOZ_CAN_RUN_SCRIPT void MaybeFlush(const PresShell& aPresShell);
385 
386     // Set to false to disallow flushing layout in some callbacks such as
387     // OnReflow(), OnScrollStart(), OnScrollStart(), or
388     // OnScrollPositionChanged().
389     bool mAllowFlushing = true;
390 
391    private:
392     // Whether we're flushing layout, used for sanity-checking.
393     bool mFlushing = false;
394   };
395 
396   LayoutFlusher mLayoutFlusher;
397 
398   // Set to True if one of the caret's position is changed in last update.
399   bool mIsCaretPositionChanged = false;
400 
401   class DesiredAsyncPanZoomState final {
402    public:
403     void Update(const AccessibleCaretManager& aAccessibleCaretManager);
404 
405     enum class Value : bool { Disabled, Enabled };
406 
Get()407     Value Get() const { return mValue; }
408 
409    private:
410     Value mValue = Value::Enabled;
411   };
412 
413   DesiredAsyncPanZoomState mDesiredAsyncPanZoomState;
414 
415   static const int32_t kAutoScrollTimerDelay = 30;
416 
417   // Clicking on the boundary of input or textarea will move the caret to the
418   // front or end of the content. To avoid this, we need to deflate the content
419   // boundary by 61 app units, which is 1 pixel + 1 app unit as defined in
420   // AppUnit.h.
421   static const int32_t kBoundaryAppUnits = 61;
422 
423   enum ScriptUpdateMode : int32_t {
424     // By default, always hide carets for selection changes due to JS calls.
425     kScriptAlwaysHide,
426     // Update any visible carets for selection changes due to JS calls,
427     // but don't show carets if carets are hidden.
428     kScriptUpdateVisible,
429     // Always show carets for selection changes due to JS calls.
430     kScriptAlwaysShow
431   };
432 };
433 
434 std::ostream& operator<<(std::ostream& aStream,
435                          const AccessibleCaretManager::CaretMode& aCaretMode);
436 
437 std::ostream& operator<<(
438     std::ostream& aStream,
439     const AccessibleCaretManager::UpdateCaretsHint& aResult);
440 
441 }  // namespace mozilla
442 
443 #endif  // AccessibleCaretManager_h
444