summaryrefslogtreecommitdiffstats
path: root/layout/base/AccessibleCaretManager.h
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--layout/base/AccessibleCaretManager.h446
1 files changed, 446 insertions, 0 deletions
diff --git a/layout/base/AccessibleCaretManager.h b/layout/base/AccessibleCaretManager.h
new file mode 100644
index 0000000000..909aeb8838
--- /dev/null
+++ b/layout/base/AccessibleCaretManager.h
@@ -0,0 +1,446 @@
+/* -*- 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();
+
+ // The canvas frame holding the accessible caret anonymous content elements
+ // was reconstructed, resulting in the content elements getting cloned.
+ virtual void OnFrameReconstruction();
+
+ // 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(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