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/. */
7 #include "AccessibleCaretEventHub.h"
9 #include "AccessibleCaretLogger.h"
10 #include "AccessibleCaretManager.h"
12 #include "mozilla/AutoRestore.h"
13 #include "mozilla/PresShell.h"
14 #include "mozilla/StaticPrefs_layout.h"
15 #include "mozilla/StaticPrefs_ui.h"
16 #include "mozilla/TextEvents.h"
17 #include "mozilla/TouchEvents.h"
18 #include "mozilla/dom/Document.h"
19 #include "mozilla/dom/MouseEventBinding.h"
20 #include "mozilla/dom/Selection.h"
21 #include "nsCanvasFrame.h"
22 #include "nsDocShell.h"
23 #include "nsFocusManager.h"
24 #include "nsFrameSelection.h"
26 #include "nsLayoutUtils.h"
27 #include "nsPresContext.h"
29 using namespace mozilla
;
30 using namespace mozilla::dom
;
35 #define AC_LOG(message, ...) \
36 AC_LOG_BASE("AccessibleCaretEventHub (%p): " message, this, ##__VA_ARGS__);
39 #define AC_LOGV(message, ...) \
40 AC_LOGV_BASE("AccessibleCaretEventHub (%p): " message, this, ##__VA_ARGS__);
42 NS_IMPL_ISUPPORTS(AccessibleCaretEventHub
, nsIReflowObserver
, nsIScrollObserver
,
43 nsISupportsWeakReference
);
45 // -----------------------------------------------------------------------------
48 class AccessibleCaretEventHub::NoActionState
49 : public AccessibleCaretEventHub::State
{
51 const char* Name() const override
{ return "NoActionState"; }
54 nsEventStatus
OnPress(AccessibleCaretEventHub
* aContext
,
55 const nsPoint
& aPoint
, int32_t aTouchId
,
56 EventClassID aEventClass
) override
{
57 nsEventStatus rv
= nsEventStatus_eIgnore
;
59 if (NS_SUCCEEDED(aContext
->mManager
->PressCaret(aPoint
, aEventClass
))) {
60 aContext
->SetState(AccessibleCaretEventHub::PressCaretState());
61 rv
= nsEventStatus_eConsumeNoDefault
;
63 aContext
->SetState(AccessibleCaretEventHub::PressNoCaretState());
66 aContext
->mPressPoint
= aPoint
;
67 aContext
->mActiveTouchId
= aTouchId
;
73 void OnScrollStart(AccessibleCaretEventHub
* aContext
) override
{
74 aContext
->mManager
->OnScrollStart();
75 aContext
->SetState(AccessibleCaretEventHub::ScrollState());
79 void OnScrollPositionChanged(AccessibleCaretEventHub
* aContext
) override
{
80 aContext
->mManager
->OnScrollPositionChanged();
84 void OnSelectionChanged(AccessibleCaretEventHub
* aContext
, Document
* aDoc
,
85 dom::Selection
* aSel
, int16_t aReason
) override
{
86 aContext
->mManager
->OnSelectionChanged(aDoc
, aSel
, aReason
);
90 void OnBlur(AccessibleCaretEventHub
* aContext
,
91 bool aIsLeavingDocument
) override
{
92 aContext
->mManager
->OnBlur();
96 void OnReflow(AccessibleCaretEventHub
* aContext
) override
{
97 aContext
->mManager
->OnReflow();
100 void Enter(AccessibleCaretEventHub
* aContext
) override
{
101 aContext
->mPressPoint
= nsPoint(NS_UNCONSTRAINEDSIZE
, NS_UNCONSTRAINEDSIZE
);
102 aContext
->mActiveTouchId
= kInvalidTouchId
;
106 // -----------------------------------------------------------------------------
107 // PressCaretState: Because we've pressed on the caret, always consume the
108 // event, both real and synthesized, so that other event handling code won't
109 // have a chance to do something else to interrupt caret dragging.
111 class AccessibleCaretEventHub::PressCaretState
112 : public AccessibleCaretEventHub::State
{
114 const char* Name() const override
{ return "PressCaretState"; }
117 nsEventStatus
OnMove(AccessibleCaretEventHub
* aContext
, const nsPoint
& aPoint
,
118 WidgetMouseEvent::Reason aReason
) override
{
119 if (aReason
== WidgetMouseEvent::eReal
&&
120 aContext
->MoveDistanceIsLarge(aPoint
)) {
121 if (NS_SUCCEEDED(aContext
->mManager
->DragCaret(aPoint
))) {
122 aContext
->SetState(AccessibleCaretEventHub::DragCaretState());
126 return nsEventStatus_eConsumeNoDefault
;
130 nsEventStatus
OnRelease(AccessibleCaretEventHub
* aContext
) override
{
131 aContext
->mManager
->ReleaseCaret();
132 aContext
->mManager
->TapCaret(aContext
->mPressPoint
);
133 aContext
->SetState(AccessibleCaretEventHub::NoActionState());
135 return nsEventStatus_eConsumeNoDefault
;
138 nsEventStatus
OnLongTap(AccessibleCaretEventHub
* aContext
,
139 const nsPoint
& aPoint
) override
{
140 return nsEventStatus_eConsumeNoDefault
;
144 // -----------------------------------------------------------------------------
145 // DragCaretState: Because we've pressed on the caret, always consume the event,
146 // both real and synthesized, so that other event handling code won't have a
147 // chance to do something else to interrupt caret dragging.
149 class AccessibleCaretEventHub::DragCaretState
150 : public AccessibleCaretEventHub::State
{
152 const char* Name() const override
{ return "DragCaretState"; }
155 nsEventStatus
OnMove(AccessibleCaretEventHub
* aContext
, const nsPoint
& aPoint
,
156 WidgetMouseEvent::Reason aReason
) override
{
157 if (aReason
== WidgetMouseEvent::eReal
) {
158 aContext
->mManager
->DragCaret(aPoint
);
161 return nsEventStatus_eConsumeNoDefault
;
165 nsEventStatus
OnRelease(AccessibleCaretEventHub
* aContext
) override
{
166 aContext
->mManager
->ReleaseCaret();
167 aContext
->SetState(AccessibleCaretEventHub::NoActionState());
169 return nsEventStatus_eConsumeNoDefault
;
173 // -----------------------------------------------------------------------------
176 class AccessibleCaretEventHub::PressNoCaretState
177 : public AccessibleCaretEventHub::State
{
179 const char* Name() const override
{ return "PressNoCaretState"; }
181 nsEventStatus
OnMove(AccessibleCaretEventHub
* aContext
, const nsPoint
& aPoint
,
182 WidgetMouseEvent::Reason aReason
) override
{
183 if (aContext
->MoveDistanceIsLarge(aPoint
)) {
184 aContext
->SetState(AccessibleCaretEventHub::NoActionState());
187 return nsEventStatus_eIgnore
;
190 nsEventStatus
OnRelease(AccessibleCaretEventHub
* aContext
) override
{
191 aContext
->SetState(AccessibleCaretEventHub::NoActionState());
193 return nsEventStatus_eIgnore
;
197 nsEventStatus
OnLongTap(AccessibleCaretEventHub
* aContext
,
198 const nsPoint
& aPoint
) override
{
199 aContext
->SetState(AccessibleCaretEventHub::LongTapState());
201 return aContext
->GetState()->OnLongTap(aContext
, aPoint
);
205 void OnScrollStart(AccessibleCaretEventHub
* aContext
) override
{
206 aContext
->mManager
->OnScrollStart();
207 aContext
->SetState(AccessibleCaretEventHub::ScrollState());
211 void OnBlur(AccessibleCaretEventHub
* aContext
,
212 bool aIsLeavingDocument
) override
{
213 aContext
->mManager
->OnBlur();
214 if (aIsLeavingDocument
) {
215 aContext
->SetState(AccessibleCaretEventHub::NoActionState());
220 void OnSelectionChanged(AccessibleCaretEventHub
* aContext
, Document
* aDoc
,
221 dom::Selection
* aSel
, int16_t aReason
) override
{
222 aContext
->mManager
->OnSelectionChanged(aDoc
, aSel
, aReason
);
226 void OnReflow(AccessibleCaretEventHub
* aContext
) override
{
227 aContext
->mManager
->OnReflow();
230 void Enter(AccessibleCaretEventHub
* aContext
) override
{
231 aContext
->LaunchLongTapInjector();
234 void Leave(AccessibleCaretEventHub
* aContext
) override
{
235 aContext
->CancelLongTapInjector();
239 // -----------------------------------------------------------------------------
242 class AccessibleCaretEventHub::ScrollState
243 : public AccessibleCaretEventHub::State
{
245 const char* Name() const override
{ return "ScrollState"; }
248 void OnScrollEnd(AccessibleCaretEventHub
* aContext
) override
{
249 aContext
->mManager
->OnScrollEnd();
250 aContext
->SetState(AccessibleCaretEventHub::NoActionState());
254 void OnScrollPositionChanged(AccessibleCaretEventHub
* aContext
) override
{
255 aContext
->mManager
->OnScrollPositionChanged();
259 void OnBlur(AccessibleCaretEventHub
* aContext
,
260 bool aIsLeavingDocument
) override
{
261 aContext
->mManager
->OnBlur();
262 if (aIsLeavingDocument
) {
263 aContext
->SetState(AccessibleCaretEventHub::NoActionState());
268 // -----------------------------------------------------------------------------
271 class AccessibleCaretEventHub::LongTapState
272 : public AccessibleCaretEventHub::State
{
274 const char* Name() const override
{ return "LongTapState"; }
277 nsEventStatus
OnLongTap(AccessibleCaretEventHub
* aContext
,
278 const nsPoint
& aPoint
) override
{
279 // In general text selection is lower-priority than the context menu. If
280 // we consume this long-press event, then it prevents the context menu from
281 // showing up on desktop Firefox (because that happens on long-tap-up, if
282 // the long-tap was not cancelled). So we return eIgnore instead.
283 aContext
->mManager
->SelectWordOrShortcut(aPoint
);
284 return nsEventStatus_eIgnore
;
287 nsEventStatus
OnRelease(AccessibleCaretEventHub
* aContext
) override
{
288 aContext
->SetState(AccessibleCaretEventHub::NoActionState());
290 // Do not consume the release since the press is not consumed in
291 // PressNoCaretState either.
292 return nsEventStatus_eIgnore
;
296 void OnScrollStart(AccessibleCaretEventHub
* aContext
) override
{
297 aContext
->mManager
->OnScrollStart();
298 aContext
->SetState(AccessibleCaretEventHub::ScrollState());
302 void OnReflow(AccessibleCaretEventHub
* aContext
) override
{
303 aContext
->mManager
->OnReflow();
307 // -----------------------------------------------------------------------------
308 // Implementation of AccessibleCaretEventHub methods
310 AccessibleCaretEventHub::State
* AccessibleCaretEventHub::GetState() const {
314 void AccessibleCaretEventHub::SetState(State
* aState
) {
317 AC_LOG("%s -> %s", mState
->Name(), aState
->Name());
324 MOZ_IMPL_STATE_CLASS_GETTER(NoActionState
)
325 MOZ_IMPL_STATE_CLASS_GETTER(PressCaretState
)
326 MOZ_IMPL_STATE_CLASS_GETTER(DragCaretState
)
327 MOZ_IMPL_STATE_CLASS_GETTER(PressNoCaretState
)
328 MOZ_IMPL_STATE_CLASS_GETTER(ScrollState
)
329 MOZ_IMPL_STATE_CLASS_GETTER(LongTapState
)
331 AccessibleCaretEventHub::AccessibleCaretEventHub(PresShell
* aPresShell
)
332 : mPresShell(aPresShell
) {}
334 void AccessibleCaretEventHub::Init() {
335 if (mInitialized
|| !mPresShell
) {
339 // Without nsAutoScriptBlocker, the script might be run after constructing
340 // mFirstCaret in AccessibleCaretManager's constructor, which might destructs
341 // the whole frame tree. Therefore we'll fail to construct mSecondCaret
342 // because we cannot get root frame or canvas frame from mPresShell to inject
343 // anonymous content. To avoid that, we protect Init() by nsAutoScriptBlocker.
344 // To reproduce, run "./mach crashtest layout/base/crashtests/897852.html"
345 // without the following scriptBlocker.
346 nsAutoScriptBlocker scriptBlocker
;
348 nsPresContext
* presContext
= mPresShell
->GetPresContext();
349 MOZ_ASSERT(presContext
, "PresContext should be given in PresShell::Init()");
351 nsDocShell
* docShell
= presContext
->GetDocShell();
356 docShell
->AddWeakReflowObserver(this);
357 docShell
->AddWeakScrollObserver(this);
359 mDocShell
= static_cast<nsDocShell
*>(docShell
);
361 if (StaticPrefs::layout_accessiblecaret_use_long_tap_injector()) {
362 mLongTapInjectorTimer
= NS_NewTimer();
365 mManager
= MakeUnique
<AccessibleCaretManager
>(mPresShell
);
370 void AccessibleCaretEventHub::Terminate() {
376 mDocShell
->RemoveWeakReflowObserver(this);
377 mDocShell
->RemoveWeakScrollObserver(this);
380 if (mLongTapInjectorTimer
) {
381 mLongTapInjectorTimer
->Cancel();
384 mManager
->Terminate();
385 mPresShell
= nullptr;
386 mInitialized
= false;
389 nsEventStatus
AccessibleCaretEventHub::HandleEvent(WidgetEvent
* aEvent
) {
390 nsEventStatus status
= nsEventStatus_eIgnore
;
396 MOZ_ASSERT(mRefCnt
.get() > 1, "Expect caller holds us as well!");
398 switch (aEvent
->mClass
) {
399 case eMouseEventClass
:
400 status
= HandleMouseEvent(aEvent
->AsMouseEvent());
403 case eTouchEventClass
:
404 status
= HandleTouchEvent(aEvent
->AsTouchEvent());
407 case eKeyboardEventClass
:
408 status
= HandleKeyboardEvent(aEvent
->AsKeyboardEvent());
412 MOZ_ASSERT_UNREACHABLE(
413 "PresShell should've filtered unwanted event classes!");
420 nsEventStatus
AccessibleCaretEventHub::HandleMouseEvent(
421 WidgetMouseEvent
* aEvent
) {
422 nsEventStatus rv
= nsEventStatus_eIgnore
;
424 if (aEvent
->mButton
!= MouseButton::ePrimary
) {
429 (mActiveTouchId
== kInvalidTouchId
? kDefaultTouchId
: mActiveTouchId
);
430 nsPoint point
= GetMouseEventPosition(aEvent
);
432 if (aEvent
->mMessage
== eMouseDown
|| aEvent
->mMessage
== eMouseUp
||
433 aEvent
->mMessage
== eMouseClick
||
434 aEvent
->mMessage
== eMouseDoubleClick
||
435 aEvent
->mMessage
== eMouseLongTap
) {
436 // Don't reset the source on mouse movement since that can
437 // happen anytime, even randomly during a touch sequence.
438 mManager
->SetLastInputSource(aEvent
->mInputSource
);
441 switch (aEvent
->mMessage
) {
443 AC_LOGV("Before eMouseDown, state: %s", mState
->Name());
444 rv
= mState
->OnPress(this, point
, id
, eMouseEventClass
);
445 AC_LOGV("After eMouseDown, state: %s, consume: %d", mState
->Name(), rv
);
449 AC_LOGV("Before eMouseMove, state: %s", mState
->Name());
450 // The mouse move events synthesized from the touch move events can have
451 // wrong point (bug 1549355). Workaround it by ignoring the events when
452 // dragging the caret because the caret doesn't really need them.
453 rv
= mState
->OnMove(this, point
, aEvent
->mReason
);
454 AC_LOGV("After eMouseMove, state: %s, consume: %d", mState
->Name(), rv
);
458 AC_LOGV("Before eMouseUp, state: %s", mState
->Name());
459 rv
= mState
->OnRelease(this);
460 AC_LOGV("After eMouseUp, state: %s, consume: %d", mState
->Name(), rv
);
464 AC_LOGV("Before eMouseLongTap, state: %s", mState
->Name());
465 rv
= mState
->OnLongTap(this, point
);
466 AC_LOGV("After eMouseLongTap, state: %s, consume: %d", mState
->Name(),
477 nsEventStatus
AccessibleCaretEventHub::HandleTouchEvent(
478 WidgetTouchEvent
* aEvent
) {
479 if (aEvent
->mTouches
.IsEmpty()) {
480 AC_LOG("%s: Receive a touch event without any touch data!", __FUNCTION__
);
481 return nsEventStatus_eIgnore
;
484 nsEventStatus rv
= nsEventStatus_eIgnore
;
487 (mActiveTouchId
== kInvalidTouchId
? aEvent
->mTouches
[0]->Identifier()
489 nsPoint point
= GetTouchEventPosition(aEvent
, id
);
491 mManager
->SetLastInputSource(MouseEvent_Binding::MOZ_SOURCE_TOUCH
);
493 switch (aEvent
->mMessage
) {
495 AC_LOGV("Before eTouchStart, state: %s", mState
->Name());
496 rv
= mState
->OnPress(this, point
, id
, eTouchEventClass
);
497 AC_LOGV("After eTouchStart, state: %s, consume: %d", mState
->Name(), rv
);
501 AC_LOGV("Before eTouchMove, state: %s", mState
->Name());
502 // There is no synthesized touch move event.
503 rv
= mState
->OnMove(this, point
, WidgetMouseEvent::eReal
);
504 AC_LOGV("After eTouchMove, state: %s, consume: %d", mState
->Name(), rv
);
508 AC_LOGV("Before eTouchEnd, state: %s", mState
->Name());
509 rv
= mState
->OnRelease(this);
510 AC_LOGV("After eTouchEnd, state: %s, consume: %d", mState
->Name(), rv
);
514 AC_LOGV("Got eTouchCancel, state: %s", mState
->Name());
515 // Do nothing since we don't really care eTouchCancel anyway.
525 nsEventStatus
AccessibleCaretEventHub::HandleKeyboardEvent(
526 WidgetKeyboardEvent
* aEvent
) {
527 mManager
->SetLastInputSource(MouseEvent_Binding::MOZ_SOURCE_KEYBOARD
);
529 switch (aEvent
->mMessage
) {
531 AC_LOGV("eKeyUp, state: %s", mState
->Name());
532 mManager
->OnKeyboardEvent();
536 AC_LOGV("eKeyDown, state: %s", mState
->Name());
537 mManager
->OnKeyboardEvent();
541 AC_LOGV("eKeyPress, state: %s", mState
->Name());
542 mManager
->OnKeyboardEvent();
549 return nsEventStatus_eIgnore
;
552 bool AccessibleCaretEventHub::MoveDistanceIsLarge(const nsPoint
& aPoint
) const {
553 nsPoint delta
= aPoint
- mPressPoint
;
554 return NS_hypot(delta
.x
, delta
.y
) >
555 AppUnitsPerCSSPixel() * kMoveStartToleranceInPixel
;
558 void AccessibleCaretEventHub::LaunchLongTapInjector() {
559 if (!mLongTapInjectorTimer
) {
563 int32_t longTapDelay
= StaticPrefs::ui_click_hold_context_menus_delay();
564 mLongTapInjectorTimer
->InitWithNamedFuncCallback(
565 FireLongTap
, this, longTapDelay
, nsITimer::TYPE_ONE_SHOT
,
566 "AccessibleCaretEventHub::LaunchLongTapInjector");
569 void AccessibleCaretEventHub::CancelLongTapInjector() {
570 if (!mLongTapInjectorTimer
) {
574 mLongTapInjectorTimer
->Cancel();
578 void AccessibleCaretEventHub::FireLongTap(nsITimer
* aTimer
,
579 void* aAccessibleCaretEventHub
) {
580 RefPtr
<AccessibleCaretEventHub
> self
=
581 static_cast<AccessibleCaretEventHub
*>(aAccessibleCaretEventHub
);
582 self
->mState
->OnLongTap(self
, self
->mPressPoint
);
586 AccessibleCaretEventHub::Reflow(DOMHighResTimeStamp aStart
,
587 DOMHighResTimeStamp aEnd
) {
592 MOZ_ASSERT(mRefCnt
.get() > 1, "Expect caller holds us as well!");
594 if (mIsInReflowCallback
) {
598 AutoRestore
<bool> autoRestoreIsInReflowCallback(mIsInReflowCallback
);
599 mIsInReflowCallback
= true;
601 AC_LOG("%s, state: %s", __FUNCTION__
, mState
->Name());
602 mState
->OnReflow(this);
607 AccessibleCaretEventHub::ReflowInterruptible(DOMHighResTimeStamp aStart
,
608 DOMHighResTimeStamp aEnd
) {
609 // Defer the error checking to Reflow().
610 return Reflow(aStart
, aEnd
);
613 void AccessibleCaretEventHub::AsyncPanZoomStarted() {
618 MOZ_ASSERT(mRefCnt
.get() > 1, "Expect caller holds us as well!");
620 AC_LOG("%s, state: %s", __FUNCTION__
, mState
->Name());
621 mState
->OnScrollStart(this);
624 void AccessibleCaretEventHub::AsyncPanZoomStopped() {
629 MOZ_ASSERT(mRefCnt
.get() > 1, "Expect caller holds us as well!");
631 AC_LOG("%s, state: %s", __FUNCTION__
, mState
->Name());
632 mState
->OnScrollEnd(this);
635 void AccessibleCaretEventHub::ScrollPositionChanged() {
640 MOZ_ASSERT(mRefCnt
.get() > 1, "Expect caller holds us as well!");
642 AC_LOG("%s, state: %s", __FUNCTION__
, mState
->Name());
643 mState
->OnScrollPositionChanged(this);
646 void AccessibleCaretEventHub::OnSelectionChange(Document
* aDoc
,
647 dom::Selection
* aSel
,
653 MOZ_ASSERT(mRefCnt
.get() > 1, "Expect caller holds us as well!");
655 AC_LOG("%s, state: %s, reason: %d", __FUNCTION__
, mState
->Name(), aReason
);
657 // XXX Here we may be in a hot path. So, if we could avoid this virtual call,
659 mState
->OnSelectionChanged(this, aDoc
, aSel
, aReason
);
662 bool AccessibleCaretEventHub::ShouldDisableApz() const {
663 return mManager
&& mManager
->ShouldDisableApz();
666 void AccessibleCaretEventHub::NotifyBlur(bool aIsLeavingDocument
) {
671 MOZ_ASSERT(mRefCnt
.get() > 1, "Expect caller holds us as well!");
673 AC_LOG("%s, state: %s", __FUNCTION__
, mState
->Name());
674 mState
->OnBlur(this, aIsLeavingDocument
);
677 nsPoint
AccessibleCaretEventHub::GetTouchEventPosition(
678 WidgetTouchEvent
* aEvent
, int32_t aIdentifier
) const {
679 for (dom::Touch
* touch
: aEvent
->mTouches
) {
680 if (touch
->Identifier() == aIdentifier
) {
681 LayoutDeviceIntPoint touchIntPoint
= touch
->mRefPoint
;
683 // Get event coordinate relative to root frame.
684 nsIFrame
* rootFrame
= mPresShell
->GetRootFrame();
685 return nsLayoutUtils::GetEventCoordinatesRelativeTo(
686 aEvent
, touchIntPoint
, RelativeTo
{rootFrame
});
689 return nsPoint(NS_UNCONSTRAINEDSIZE
, NS_UNCONSTRAINEDSIZE
);
692 nsPoint
AccessibleCaretEventHub::GetMouseEventPosition(
693 WidgetMouseEvent
* aEvent
) const {
694 LayoutDeviceIntPoint mouseIntPoint
= aEvent
->AsGUIEvent()->mRefPoint
;
696 // Get event coordinate relative to root frame.
697 nsIFrame
* rootFrame
= mPresShell
->GetRootFrame();
698 return nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent
, mouseIntPoint
,
699 RelativeTo
{rootFrame
});
702 } // namespace mozilla