gecko/layout/base/TouchCaret.cpp

1204 lines
34 KiB
C++

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 sw=2 et tw=78: */
/* 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/. */
#include "prlog.h"
#include "TouchCaret.h"
#include <algorithm>
#include "nsBlockFrame.h"
#include "nsCanvasFrame.h"
#include "nsCaret.h"
#include "nsCOMPtr.h"
#include "nsContentUtils.h"
#include "nsDOMTokenList.h"
#include "nsFrameSelection.h"
#include "nsIContent.h"
#include "nsIDOMNode.h"
#include "nsIDOMWindow.h"
#include "nsIFrame.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsIPresShell.h"
#include "nsIScrollableFrame.h"
#include "nsISelection.h"
#include "nsISelectionController.h"
#include "nsISelectionPrivate.h"
#include "nsPresContext.h"
#include "nsQueryContentEventResult.h"
#include "nsView.h"
#include "mozilla/dom/SelectionStateChangedEvent.h"
#include "mozilla/dom/CustomEvent.h"
#include "mozilla/BasicEvents.h"
#include "mozilla/Preferences.h"
using namespace mozilla;
static PRLogModuleInfo* gTouchCaretLog;
static const char* kTouchCaretLogModuleName = "TouchCaret";
// To enable all the TOUCHCARET_LOG print statements, set the environment
// variable NSPR_LOG_MODULES=TouchCaret:5
#define TOUCHCARET_LOG(message, ...) \
PR_LOG(gTouchCaretLog, PR_LOG_DEBUG, \
("TouchCaret (%p): %s:%d : " message "\n", this, __FUNCTION__, \
__LINE__, ##__VA_ARGS__));
#define TOUCHCARET_LOG_STATIC(message, ...) \
PR_LOG(gTouchCaretLog, PR_LOG_DEBUG, \
("TouchCaret: %s:%d : " message "\n", __FUNCTION__, __LINE__, \
##__VA_ARGS__));
// Click on the boundary of input/textarea will place the caret at the
// front/end of the content. To advoid this, we need to deflate the content
// boundary by 61 app units (1 pixel + 1 app unit).
static const int32_t kBoundaryAppUnits = 61;
NS_IMPL_ISUPPORTS(TouchCaret,
nsISelectionListener,
nsIScrollObserver,
nsISupportsWeakReference)
/*static*/ int32_t TouchCaret::sTouchCaretInflateSize = 0;
/*static*/ int32_t TouchCaret::sTouchCaretExpirationTime = 0;
/*static*/ bool TouchCaret::sCaretManagesAndroidActionbar = false;
/*static*/ bool TouchCaret::sTouchcaretExtendedvisibility = false;
/*static*/ uint32_t TouchCaret::sActionBarViewCount = 0;
TouchCaret::TouchCaret(nsIPresShell* aPresShell)
: mState(TOUCHCARET_NONE),
mActiveTouchId(-1),
mCaretCenterToDownPointOffsetY(0),
mInAsyncPanZoomGesture(false),
mVisible(false),
mIsValidTap(false),
mActionBarViewID(0)
{
MOZ_ASSERT(NS_IsMainThread());
if (!gTouchCaretLog) {
gTouchCaretLog = PR_NewLogModule(kTouchCaretLogModuleName);
}
TOUCHCARET_LOG("Constructor, PresShell=%p", aPresShell);
static bool addedTouchCaretPref = false;
if (!addedTouchCaretPref) {
Preferences::AddIntVarCache(&sTouchCaretInflateSize,
"touchcaret.inflatesize.threshold");
Preferences::AddIntVarCache(&sTouchCaretExpirationTime,
"touchcaret.expiration.time");
Preferences::AddBoolVarCache(&sCaretManagesAndroidActionbar,
"caret.manages-android-actionbar");
Preferences::AddBoolVarCache(&sTouchcaretExtendedvisibility,
"touchcaret.extendedvisibility");
addedTouchCaretPref = true;
}
// The presshell owns us, so no addref.
mPresShell = do_GetWeakReference(aPresShell);
MOZ_ASSERT(mPresShell, "Hey, pres shell should support weak refs");
}
void
TouchCaret::Init()
{
nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
if (!presShell) {
return;
}
nsPresContext* presContext = presShell->GetPresContext();
MOZ_ASSERT(presContext, "PresContext should be given in PresShell::Init()");
nsIDocShell* docShell = presContext->GetDocShell();
if (!docShell) {
return;
}
docShell->AddWeakScrollObserver(this);
mDocShell = static_cast<nsDocShell*>(docShell);
}
void
TouchCaret::Terminate()
{
nsRefPtr<nsDocShell> docShell(mDocShell.get());
if (docShell) {
docShell->RemoveWeakScrollObserver(this);
}
if (mScrollEndDetectorTimer) {
mScrollEndDetectorTimer->Cancel();
mScrollEndDetectorTimer = nullptr;
}
mDocShell = WeakPtr<nsDocShell>();
mPresShell = nullptr;
}
TouchCaret::~TouchCaret()
{
TOUCHCARET_LOG("Destructor");
MOZ_ASSERT(NS_IsMainThread());
if (mTouchCaretExpirationTimer) {
mTouchCaretExpirationTimer->Cancel();
mTouchCaretExpirationTimer = nullptr;
}
}
nsIFrame*
TouchCaret::GetCaretFocusFrame(nsRect* aOutRect)
{
nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
if (!presShell) {
return nullptr;
}
nsRefPtr<nsCaret> caret = presShell->GetCaret();
if (!caret) {
return nullptr;
}
nsRect rect;
nsIFrame* frame = caret->GetGeometry(&rect);
if (aOutRect) {
*aOutRect = rect;
}
return frame;
}
nsCanvasFrame*
TouchCaret::GetCanvasFrame()
{
nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
if (!presShell) {
return nullptr;
}
return presShell->GetCanvasFrame();
}
nsIFrame*
TouchCaret::GetRootFrame()
{
nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
if (!presShell) {
return nullptr;
}
return presShell->GetRootFrame();
}
void
TouchCaret::SetVisibility(bool aVisible)
{
if (mVisible == aVisible) {
TOUCHCARET_LOG("Set visibility %s, same as the old one",
(aVisible ? "shown" : "hidden"));
return;
}
nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
if (!presShell) {
return;
}
mozilla::dom::Element* touchCaretElement = presShell->GetTouchCaretElement();
if (!touchCaretElement) {
return;
}
mVisible = aVisible;
// Set touch caret visibility.
ErrorResult err;
touchCaretElement->ClassList()->Toggle(NS_LITERAL_STRING("hidden"),
dom::Optional<bool>(!mVisible),
err);
TOUCHCARET_LOG("Set visibility %s", (mVisible ? "shown" : "hidden"));
// Set touch caret expiration time.
mVisible ? LaunchExpirationTimer() : CancelExpirationTimer();
// If after a TouchCaret visibility change we become hidden, ensure
// the Android ActionBar handler is notified to close the current view.
if (!mVisible && sCaretManagesAndroidActionbar) {
UpdateAndroidActionBarVisibility(false, mActionBarViewID);
}
}
/**
* Open or close the Android TextSelection ActionBar, based on visibility.
* Each time we're called to open the actionbar, we increment / assign a
* unique view ID and return it to the caller. The ID is returned on calls
* to close the actionbar to ensure we don't close the shared view if it
* was already force closed by a subsequent callers open request.
*/
/* static */void
TouchCaret::UpdateAndroidActionBarVisibility(bool aVisibility, uint32_t& aViewID)
{
// Are we openning a new view?
if (aVisibility) {
// Assign a new view ID.
aViewID = ++sActionBarViewCount;
}
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
if (os) {
nsString topic = (aVisibility) ?
NS_LITERAL_STRING("ActionBar:OpenNew") : NS_LITERAL_STRING("ActionBar:Close");
nsAutoString viewCount;
viewCount.AppendInt(aViewID);
os->NotifyObservers(nullptr, NS_ConvertUTF16toUTF8(topic).get(), viewCount.get());
}
}
nsRect
TouchCaret::GetTouchFrameRect()
{
nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
if (!presShell) {
return nsRect();
}
dom::Element* touchCaretElement = presShell->GetTouchCaretElement();
nsIFrame* canvasFrame = GetCanvasFrame();
return nsLayoutUtils::GetRectRelativeToFrame(touchCaretElement, canvasFrame);
}
nsRect
TouchCaret::GetContentBoundary()
{
nsIFrame* focusFrame = GetCaretFocusFrame();
nsIFrame* canvasFrame = GetCanvasFrame();
if (!focusFrame || !canvasFrame) {
return nsRect();
}
// Get the editing host to determine the touch caret dragable boundary.
dom::Element* editingHost = focusFrame->GetContent()->GetEditingHost();
if (!editingHost) {
return nsRect();
}
nsRect resultRect;
for (nsIFrame* frame = editingHost->GetPrimaryFrame(); frame;
frame = frame->GetNextContinuation()) {
nsRect rect = frame->GetContentRectRelativeToSelf();
nsLayoutUtils::TransformRect(frame, canvasFrame, rect);
resultRect = resultRect.Union(rect);
mozilla::layout::FrameChildListIterator lists(frame);
for (; !lists.IsDone(); lists.Next()) {
// Loop over all children to take the overflow rect in to consideration.
nsFrameList::Enumerator childFrames(lists.CurrentList());
for (; !childFrames.AtEnd(); childFrames.Next()) {
nsIFrame* kid = childFrames.get();
nsRect overflowRect = kid->GetScrollableOverflowRect();
nsLayoutUtils::TransformRect(kid, canvasFrame, overflowRect);
resultRect = resultRect.Union(overflowRect);
}
}
}
// Shrink rect to make sure we never hit the boundary.
resultRect.Deflate(kBoundaryAppUnits);
return resultRect;
}
nscoord
TouchCaret::GetCaretYCenterPosition()
{
nsRect caretRect;
nsIFrame* focusFrame = GetCaretFocusFrame(&caretRect);
nsIFrame* canvasFrame = GetCanvasFrame();
nsLayoutUtils::TransformRect(focusFrame, canvasFrame, caretRect);
return (caretRect.y + caretRect.height / 2);
}
void
TouchCaret::SetTouchFramePos(const nsRect& aCaretRect)
{
nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
if (!presShell) {
return;
}
mozilla::dom::Element* touchCaretElement = presShell->GetTouchCaretElement();
if (!touchCaretElement) {
return;
}
// Convert aOrigin to CSS pixels.
nsRefPtr<nsPresContext> presContext = presShell->GetPresContext();
int32_t x = presContext->AppUnitsToIntCSSPixels(aCaretRect.Center().x);
int32_t y = presContext->AppUnitsToIntCSSPixels(aCaretRect.y);
int32_t padding = presContext->AppUnitsToIntCSSPixels(aCaretRect.height);
nsAutoString styleStr;
styleStr.AppendLiteral("left: ");
styleStr.AppendInt(x);
styleStr.AppendLiteral("px; top: ");
styleStr.AppendInt(y);
styleStr.AppendLiteral("px; padding-top: ");
styleStr.AppendInt(padding);
styleStr.AppendLiteral("px;");
TOUCHCARET_LOG("Set style: %s", NS_ConvertUTF16toUTF8(styleStr).get());
touchCaretElement->SetAttr(kNameSpaceID_None, nsGkAtoms::style,
styleStr, true);
}
void
TouchCaret::MoveCaret(const nsPoint& movePoint)
{
nsIFrame* focusFrame = GetCaretFocusFrame();
nsIFrame* canvasFrame = GetCanvasFrame();
if (!focusFrame && !canvasFrame) {
return;
}
nsIFrame* scrollable =
nsLayoutUtils::GetClosestFrameOfType(focusFrame, nsGkAtoms::scrollFrame);
// Convert touch/mouse position to frame coordinates.
nsPoint offsetToCanvasFrame = nsPoint(0,0);
nsLayoutUtils::TransformPoint(scrollable, canvasFrame, offsetToCanvasFrame);
nsPoint pt = movePoint - offsetToCanvasFrame;
// Evaluate offsets.
nsIFrame::ContentOffsets offsets =
scrollable->GetContentOffsetsFromPoint(pt, nsIFrame::SKIP_HIDDEN);
// Move caret position.
nsWeakFrame weakScrollable = scrollable;
nsRefPtr<nsFrameSelection> fs = scrollable->GetFrameSelection();
fs->HandleClick(offsets.content, offsets.StartOffset(),
offsets.EndOffset(),
false,
false,
offsets.associate);
if (!weakScrollable.IsAlive()) {
return;
}
// Scroll scrolled frame.
nsIScrollableFrame* saf = do_QueryFrame(scrollable);
nsIFrame* capturingFrame = saf->GetScrolledFrame();
offsetToCanvasFrame = nsPoint(0,0);
nsLayoutUtils::TransformPoint(capturingFrame, canvasFrame, offsetToCanvasFrame);
pt = movePoint - offsetToCanvasFrame;
fs->StartAutoScrollTimer(capturingFrame, pt, sAutoScrollTimerDelay);
}
bool
TouchCaret::IsOnTouchCaret(const nsPoint& aPoint)
{
return mVisible && nsLayoutUtils::ContainsPoint(GetTouchFrameRect(), aPoint,
TouchCaretInflateSize());
}
nsresult
TouchCaret::NotifySelectionChanged(nsIDOMDocument* aDoc, nsISelection* aSel,
int16_t aReason)
{
TOUCHCARET_LOG("aSel (%p), Reason=%d", aSel, aReason);
// Hide touch caret while no caret exists.
nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
if (!presShell) {
return NS_OK;
}
nsRefPtr<nsCaret> caret = presShell->GetCaret();
if (!caret) {
SetVisibility(false);
return NS_OK;
}
// The same touch caret is shared amongst the document and any text widgets it
// may contain. This means that the touch caret could get notifications from
// multiple selections.
// If this notification is for a selection that is not the one the
// the caret is currently interested in , then there is nothing to do!
if (aSel != caret->GetSelection()) {
TOUCHCARET_LOG("Return for selection mismatch!");
return NS_OK;
}
// Update touch caret position and visibility.
// Hide touch caret while key event causes selection change.
// Also hide touch caret when gecko or javascript collapse the selection.
if (aReason & nsISelectionListener::KEYPRESS_REASON ||
aReason & nsISelectionListener::COLLAPSETOSTART_REASON ||
aReason & nsISelectionListener::COLLAPSETOEND_REASON) {
TOUCHCARET_LOG("KEYPRESS_REASON");
SetVisibility(false);
} else {
SyncVisibilityWithCaret();
// Is the TouchCaret visible and we're showing/hiding the actionbar?
if (mVisible && sCaretManagesAndroidActionbar) {
// A selection change due to touch tap opens the actionbar.
if (aReason & nsISelectionListener::MOUSEUP_REASON) {
UpdateAndroidActionBarVisibility(true, mActionBarViewID);
} else {
// Update the ActionBar state for caret-specific selection changes.
// Ignore transient selection composition changes that occur while
// the TouchCaret is also visible.
bool isCollapsed;
if (NS_SUCCEEDED(aSel->GetIsCollapsed(&isCollapsed)) && isCollapsed) {
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
if (os) {
os->NotifyObservers(nullptr, "ActionBar:UpdateState", nullptr);
}
}
}
}
}
return NS_OK;
}
/**
* Used to update caret position after PanZoom stops for
* extended caret visibility. Never needed by MOZ_WIDGET_GONK.
*/
void
TouchCaret::AsyncPanZoomStarted()
{
if (mVisible) {
if (sTouchcaretExtendedvisibility) {
mInAsyncPanZoomGesture = true;
}
}
}
void
TouchCaret::AsyncPanZoomStopped()
{
if (mInAsyncPanZoomGesture) {
mInAsyncPanZoomGesture = false;
UpdatePosition();
}
}
/**
* Used to update caret position after Scroll stops for
* extended caret visibility. Never needed by MOZ_WIDGET_GONK.
*/
void
TouchCaret::ScrollPositionChanged()
{
if (mVisible) {
if (sTouchcaretExtendedvisibility) {
// Launch scroll end detector.
LaunchScrollEndDetector();
}
}
}
void
TouchCaret::LaunchScrollEndDetector()
{
if (!mScrollEndDetectorTimer) {
mScrollEndDetectorTimer = do_CreateInstance("@mozilla.org/timer;1");
}
MOZ_ASSERT(mScrollEndDetectorTimer);
mScrollEndDetectorTimer->InitWithFuncCallback(FireScrollEnd,
this,
sScrollEndTimerDelay,
nsITimer::TYPE_ONE_SHOT);
}
void
TouchCaret::CancelScrollEndDetector()
{
if (mScrollEndDetectorTimer) {
mScrollEndDetectorTimer->Cancel();
}
}
/* static */void
TouchCaret::FireScrollEnd(nsITimer* aTimer, void* aTouchCaret)
{
nsRefPtr<TouchCaret> self = static_cast<TouchCaret*>(aTouchCaret);
NS_PRECONDITION(aTimer == self->mScrollEndDetectorTimer,
"Unexpected timer");
self->UpdatePosition();
}
void
TouchCaret::SyncVisibilityWithCaret()
{
TOUCHCARET_LOG("SyncVisibilityWithCaret");
if (!IsDisplayable()) {
SetVisibility(false);
return;
}
SetVisibility(true);
if (mVisible) {
UpdatePosition();
}
}
void
TouchCaret::UpdatePositionIfNeeded()
{
TOUCHCARET_LOG("UpdatePositionIfNeeded");
if (!IsDisplayable()) {
SetVisibility(false);
return;
}
if (mVisible) {
UpdatePosition();
}
}
bool
TouchCaret::IsDisplayable()
{
nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
if (!presShell) {
TOUCHCARET_LOG("PresShell is nullptr!");
return false;
}
nsRefPtr<nsCaret> caret = presShell->GetCaret();
if (!caret) {
TOUCHCARET_LOG("Caret is nullptr!");
return false;
}
nsIFrame* canvasFrame = GetCanvasFrame();
if (!canvasFrame) {
TOUCHCARET_LOG("No canvas frame!");
return false;
}
nsIFrame* rootFrame = GetRootFrame();
if (!rootFrame) {
TOUCHCARET_LOG("No root frame!");
return false;
}
dom::Element* touchCaretElement = presShell->GetTouchCaretElement();
if (!touchCaretElement) {
TOUCHCARET_LOG("No touch caret frame element!");
return false;
}
if (presShell->IsPaintingSuppressed()) {
TOUCHCARET_LOG("PresShell is suppressing painting!");
return false;
}
if (!caret->IsVisible()) {
TOUCHCARET_LOG("Caret is not visible!");
return false;
}
nsRect focusRect;
nsIFrame* focusFrame = caret->GetGeometry(&focusRect);
if (!focusFrame) {
TOUCHCARET_LOG("Focus frame is not valid!");
return false;
}
dom::Element* editingHost = focusFrame->GetContent()->GetEditingHost();
if (!editingHost) {
TOUCHCARET_LOG("Cannot get editing host!");
return false;
}
// No further checks required if extended TouchCaret visibility.
if (sTouchcaretExtendedvisibility) {
return true;
}
if (focusRect.IsEmpty()) {
TOUCHCARET_LOG("Focus rect is empty!");
return false;
}
if (!nsContentUtils::HasNonEmptyTextContent(
editingHost, nsContentUtils::eRecurseIntoChildren)) {
TOUCHCARET_LOG("The content is empty!");
return false;
}
if (mState != TOUCHCARET_TOUCHDRAG_ACTIVE &&
!nsLayoutUtils::IsRectVisibleInScrollFrames(focusFrame, focusRect)) {
TOUCHCARET_LOG("Caret does not show in the scrollable frame!");
return false;
}
TOUCHCARET_LOG("Touch caret is displayable!");
return true;
}
void
TouchCaret::UpdatePosition()
{
MOZ_ASSERT(mVisible);
nsRect rect = GetTouchCaretRect();
rect = ClampRectToScrollFrame(rect);
SetTouchFramePos(rect);
}
nsRect
TouchCaret::GetTouchCaretRect()
{
nsRect focusRect;
nsIFrame* focusFrame = GetCaretFocusFrame(&focusRect);
nsIFrame* rootFrame = GetRootFrame();
// Transform the position to make it relative to root frame.
nsLayoutUtils::TransformRect(focusFrame, rootFrame, focusRect);
return focusRect;
}
nsRect
TouchCaret::ClampRectToScrollFrame(const nsRect& aRect)
{
nsRect rect = aRect;
nsIFrame* focusFrame = GetCaretFocusFrame();
nsIFrame* rootFrame = GetRootFrame();
// Clamp the touch caret position to the scrollframe boundary.
nsIFrame* closestScrollFrame =
nsLayoutUtils::GetClosestFrameOfType(focusFrame, nsGkAtoms::scrollFrame);
while (closestScrollFrame) {
nsIScrollableFrame* sf = do_QueryFrame(closestScrollFrame);
nsRect visualRect = sf->GetScrollPortRect();
// Clamp the touch caret in the scroll port.
nsLayoutUtils::TransformRect(closestScrollFrame, rootFrame, visualRect);
rect = rect.Intersect(visualRect);
// Get next ancestor scroll frame.
closestScrollFrame =
nsLayoutUtils::GetClosestFrameOfType(closestScrollFrame->GetParent(),
nsGkAtoms::scrollFrame);
}
return rect;
}
/* static */void
TouchCaret::DisableTouchCaretCallback(nsITimer* aTimer, void* aTouchCaret)
{
nsRefPtr<TouchCaret> self = static_cast<TouchCaret*>(aTouchCaret);
NS_PRECONDITION(aTimer == self->mTouchCaretExpirationTimer,
"Unexpected timer");
self->SetVisibility(false);
}
void
TouchCaret::LaunchExpirationTimer()
{
if (TouchCaretExpirationTime() > 0) {
if (!mTouchCaretExpirationTimer) {
mTouchCaretExpirationTimer = do_CreateInstance("@mozilla.org/timer;1");
}
if (mTouchCaretExpirationTimer) {
mTouchCaretExpirationTimer->Cancel();
mTouchCaretExpirationTimer->InitWithFuncCallback(DisableTouchCaretCallback,
this,
TouchCaretExpirationTime(),
nsITimer::TYPE_ONE_SHOT);
}
}
}
void
TouchCaret::CancelExpirationTimer()
{
if (mTouchCaretExpirationTimer) {
mTouchCaretExpirationTimer->Cancel();
}
}
void
TouchCaret::SetSelectionDragState(bool aState)
{
nsIFrame* caretFocusFrame = GetCaretFocusFrame();
if (!caretFocusFrame) {
return;
}
nsRefPtr<nsFrameSelection> fs = caretFocusFrame->GetFrameSelection();
fs->SetDragState(aState);
}
nsEventStatus
TouchCaret::HandleEvent(WidgetEvent* aEvent)
{
MOZ_ASSERT(NS_IsMainThread());
if (!IsDisplayable()) {
return nsEventStatus_eIgnore;
}
nsEventStatus status = nsEventStatus_eIgnore;
switch (aEvent->message) {
case NS_TOUCH_START:
status = HandleTouchDownEvent(aEvent->AsTouchEvent());
break;
case NS_MOUSE_BUTTON_DOWN:
status = HandleMouseDownEvent(aEvent->AsMouseEvent());
break;
case NS_TOUCH_END:
status = HandleTouchUpEvent(aEvent->AsTouchEvent());
break;
case NS_MOUSE_BUTTON_UP:
status = HandleMouseUpEvent(aEvent->AsMouseEvent());
break;
case NS_TOUCH_MOVE:
status = HandleTouchMoveEvent(aEvent->AsTouchEvent());
break;
case NS_MOUSE_MOVE:
status = HandleMouseMoveEvent(aEvent->AsMouseEvent());
break;
case NS_TOUCH_CANCEL:
mTouchesId.Clear();
SetState(TOUCHCARET_NONE);
LaunchExpirationTimer();
break;
case NS_KEY_UP:
case NS_KEY_DOWN:
case NS_KEY_PRESS:
case NS_WHEEL_WHEEL:
case NS_WHEEL_START:
case NS_WHEEL_STOP:
// Disable touch caret while key/wheel event is received.
TOUCHCARET_LOG("Receive key/wheel event %d", aEvent->message);
SetVisibility(false);
break;
case NS_MOUSE_MOZLONGTAP:
if (mState == TOUCHCARET_TOUCHDRAG_ACTIVE) {
// Disable long tap event from APZ while dragging the touch caret.
status = nsEventStatus_eConsumeNoDefault;
}
break;
default:
break;
}
return status;
}
nsPoint
TouchCaret::GetEventPosition(WidgetTouchEvent* aEvent, int32_t aIdentifier)
{
for (size_t i = 0; i < aEvent->touches.Length(); i++) {
if (aEvent->touches[i]->mIdentifier == aIdentifier) {
// Get event coordinate relative to canvas frame.
nsIFrame* canvasFrame = GetCanvasFrame();
LayoutDeviceIntPoint touchIntPoint = aEvent->touches[i]->mRefPoint;
return nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent,
touchIntPoint,
canvasFrame);
}
}
return nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE);
}
nsPoint
TouchCaret::GetEventPosition(WidgetMouseEvent* aEvent)
{
// Get event coordinate relative to canvas frame.
nsIFrame* canvasFrame = GetCanvasFrame();
LayoutDeviceIntPoint mouseIntPoint = aEvent->AsGUIEvent()->refPoint;
return nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent,
mouseIntPoint,
canvasFrame);
}
nsEventStatus
TouchCaret::HandleMouseMoveEvent(WidgetMouseEvent* aEvent)
{
TOUCHCARET_LOG("Got a mouse-move in state %d", mState);
nsEventStatus status = nsEventStatus_eIgnore;
switch (mState) {
case TOUCHCARET_NONE:
break;
case TOUCHCARET_MOUSEDRAG_ACTIVE:
{
nsPoint movePoint = GetEventPosition(aEvent);
movePoint.y += mCaretCenterToDownPointOffsetY;
nsRect contentBoundary = GetContentBoundary();
movePoint = contentBoundary.ClampPoint(movePoint);
MoveCaret(movePoint);
mIsValidTap = false;
status = nsEventStatus_eConsumeNoDefault;
}
break;
case TOUCHCARET_TOUCHDRAG_ACTIVE:
case TOUCHCARET_TOUCHDRAG_INACTIVE:
// Consume mouse event in touch sequence.
status = nsEventStatus_eConsumeNoDefault;
break;
}
return status;
}
nsEventStatus
TouchCaret::HandleTouchMoveEvent(WidgetTouchEvent* aEvent)
{
TOUCHCARET_LOG("Got a touch-move in state %d", mState);
nsEventStatus status = nsEventStatus_eIgnore;
switch (mState) {
case TOUCHCARET_NONE:
break;
case TOUCHCARET_MOUSEDRAG_ACTIVE:
// Consume touch event in mouse sequence.
status = nsEventStatus_eConsumeNoDefault;
break;
case TOUCHCARET_TOUCHDRAG_ACTIVE:
{
nsPoint movePoint = GetEventPosition(aEvent, mActiveTouchId);
movePoint.y += mCaretCenterToDownPointOffsetY;
nsRect contentBoundary = GetContentBoundary();
movePoint = contentBoundary.ClampPoint(movePoint);
MoveCaret(movePoint);
mIsValidTap = false;
status = nsEventStatus_eConsumeNoDefault;
}
break;
case TOUCHCARET_TOUCHDRAG_INACTIVE:
// Consume NS_TOUCH_MOVE event in TOUCHCARET_TOUCHDRAG_INACTIVE state.
status = nsEventStatus_eConsumeNoDefault;
break;
}
return status;
}
nsEventStatus
TouchCaret::HandleMouseUpEvent(WidgetMouseEvent* aEvent)
{
TOUCHCARET_LOG("Got a mouse-up in state %d", mState);
nsEventStatus status = nsEventStatus_eIgnore;
switch (mState) {
case TOUCHCARET_NONE:
break;
case TOUCHCARET_MOUSEDRAG_ACTIVE:
if (aEvent->button == WidgetMouseEvent::eLeftButton) {
SetSelectionDragState(false);
LaunchExpirationTimer();
SetState(TOUCHCARET_NONE);
status = nsEventStatus_eConsumeNoDefault;
}
break;
case TOUCHCARET_TOUCHDRAG_ACTIVE:
case TOUCHCARET_TOUCHDRAG_INACTIVE:
// Consume mouse event in touch sequence.
status = nsEventStatus_eConsumeNoDefault;
break;
}
return status;
}
nsEventStatus
TouchCaret::HandleTouchUpEvent(WidgetTouchEvent* aEvent)
{
TOUCHCARET_LOG("Got a touch-end in state %d", mState);
// Remove touches from cache if the stroke is gone in TOUCHDRAG states.
if (mState == TOUCHCARET_TOUCHDRAG_ACTIVE ||
mState == TOUCHCARET_TOUCHDRAG_INACTIVE) {
for (size_t i = 0; i < aEvent->touches.Length(); i++) {
nsTArray<int32_t>::index_type index =
mTouchesId.IndexOf(aEvent->touches[i]->mIdentifier);
MOZ_ASSERT(index != nsTArray<int32_t>::NoIndex);
mTouchesId.RemoveElementAt(index);
}
}
nsEventStatus status = nsEventStatus_eIgnore;
switch (mState) {
case TOUCHCARET_NONE:
break;
case TOUCHCARET_MOUSEDRAG_ACTIVE:
// Consume touch event in mouse sequence.
status = nsEventStatus_eConsumeNoDefault;
break;
case TOUCHCARET_TOUCHDRAG_ACTIVE:
if (mTouchesId.Length() == 0) {
SetSelectionDragState(false);
// No more finger on the screen.
SetState(TOUCHCARET_NONE);
LaunchExpirationTimer();
} else {
// Still has finger touching on the screen.
if (aEvent->touches[0]->mIdentifier == mActiveTouchId) {
// Remove finger from the touch caret.
SetState(TOUCHCARET_TOUCHDRAG_INACTIVE);
LaunchExpirationTimer();
} else {
// If the finger removed is not the finger on touch caret, remain in
// TOUCHCARET_DRAG_ACTIVE state.
}
}
status = nsEventStatus_eConsumeNoDefault;
break;
case TOUCHCARET_TOUCHDRAG_INACTIVE:
if (mTouchesId.Length() == 0) {
// No more finger on the screen.
SetState(TOUCHCARET_NONE);
}
status = nsEventStatus_eConsumeNoDefault;
break;
}
return status;
}
nsEventStatus
TouchCaret::HandleMouseDownEvent(WidgetMouseEvent* aEvent)
{
TOUCHCARET_LOG("Got a mouse-down in state %d", mState);
if (!GetVisibility()) {
// If touch caret is invisible, bypass event.
return nsEventStatus_eIgnore;
}
nsEventStatus status = nsEventStatus_eIgnore;
switch (mState) {
case TOUCHCARET_NONE:
if (aEvent->button == WidgetMouseEvent::eLeftButton) {
nsPoint point = GetEventPosition(aEvent);
if (IsOnTouchCaret(point)) {
SetSelectionDragState(true);
// Cache distence of the event point to the center of touch caret.
mCaretCenterToDownPointOffsetY = GetCaretYCenterPosition() - point.y;
// Enter TOUCHCARET_MOUSEDRAG_ACTIVE state and cancel the timer.
SetState(TOUCHCARET_MOUSEDRAG_ACTIVE);
CancelExpirationTimer();
status = nsEventStatus_eConsumeNoDefault;
} else {
// Mousedown events that miss HitTest can be caused by soft-keyboard
// auto-suggestions. If extended visibility, update the caret position.
if (sTouchcaretExtendedvisibility) {
UpdatePositionIfNeeded();
break;
}
// Set touch caret invisible if HisTest fails. Bypass event.
SetVisibility(false);
status = nsEventStatus_eIgnore;
}
} else {
// Set touch caret invisible if not left button down event.
SetVisibility(false);
status = nsEventStatus_eIgnore;
}
break;
case TOUCHCARET_MOUSEDRAG_ACTIVE:
SetVisibility(false);
SetState(TOUCHCARET_NONE);
break;
case TOUCHCARET_TOUCHDRAG_ACTIVE:
case TOUCHCARET_TOUCHDRAG_INACTIVE:
// Consume mouse event in touch sequence.
status = nsEventStatus_eConsumeNoDefault;
break;
}
return status;
}
nsEventStatus
TouchCaret::HandleTouchDownEvent(WidgetTouchEvent* aEvent)
{
TOUCHCARET_LOG("Got a touch-start in state %d", mState);
nsEventStatus status = nsEventStatus_eIgnore;
switch (mState) {
case TOUCHCARET_NONE:
if (!GetVisibility()) {
// If touch caret is invisible, bypass event.
status = nsEventStatus_eIgnore;
} else {
// Loop over all the touches and see if any of them is on the touch
// caret.
for (size_t i = 0; i < aEvent->touches.Length(); ++i) {
int32_t touchId = aEvent->touches[i]->Identifier();
nsPoint point = GetEventPosition(aEvent, touchId);
if (IsOnTouchCaret(point)) {
SetSelectionDragState(true);
// Touch start position is contained in touch caret.
mActiveTouchId = touchId;
// Cache distance of the event point to the center of touch caret.
mCaretCenterToDownPointOffsetY = GetCaretYCenterPosition() - point.y;
// Enter TOUCHCARET_TOUCHDRAG_ACTIVE state and cancel the timer.
SetState(TOUCHCARET_TOUCHDRAG_ACTIVE);
CancelExpirationTimer();
status = nsEventStatus_eConsumeNoDefault;
break;
}
}
// No touch is on the touch caret. Set touch caret invisible, and bypass
// the event.
if (mActiveTouchId == -1) {
// Check touch caret visibility style.
if (sTouchcaretExtendedvisibility) {
// Update position on events associated with scroll and pan-zoom.
UpdatePositionIfNeeded();
} else {
SetVisibility(false);
status = nsEventStatus_eIgnore;
}
}
}
break;
case TOUCHCARET_MOUSEDRAG_ACTIVE:
case TOUCHCARET_TOUCHDRAG_ACTIVE:
case TOUCHCARET_TOUCHDRAG_INACTIVE:
// Consume NS_TOUCH_START event.
status = nsEventStatus_eConsumeNoDefault;
break;
}
// Cache active touch IDs in TOUCHDRAG states.
if (mState == TOUCHCARET_TOUCHDRAG_ACTIVE ||
mState == TOUCHCARET_TOUCHDRAG_INACTIVE) {
mTouchesId.Clear();
for (size_t i = 0; i < aEvent->touches.Length(); i++) {
mTouchesId.AppendElement(aEvent->touches[i]->mIdentifier);
}
}
return status;
}
void
TouchCaret::DispatchTapEvent()
{
nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
if (!presShell) {
return;
}
nsRefPtr<nsCaret> caret = presShell->GetCaret();
if (!caret) {
return;
}
dom::Selection* sel = static_cast<dom::Selection*>(caret->GetSelection());
if (!sel) {
return;
}
nsIDocument* doc = presShell->GetDocument();
MOZ_ASSERT(doc);
dom::SelectionStateChangedEventInit init;
init.mBubbles = true;
// XXX: Do we need to flush layout?
presShell->FlushPendingNotifications(Flush_Layout);
nsRect rect = nsContentUtils::GetSelectionBoundingRect(sel);
nsRefPtr<dom::DOMRect>domRect = new dom::DOMRect(ToSupports(doc));
domRect->SetLayoutRect(rect);
init.mBoundingClientRect = domRect;
init.mVisible = false;
sel->Stringify(init.mSelectedText);
dom::Sequence<dom::SelectionState> state;
state.AppendElement(dom::SelectionState::Taponcaret);
init.mStates = state;
nsRefPtr<dom::SelectionStateChangedEvent> event =
dom::SelectionStateChangedEvent::Constructor(doc, NS_LITERAL_STRING("mozselectionstatechanged"), init);
event->SetTrusted(true);
event->GetInternalNSEvent()->mFlags.mOnlyChromeDispatch = true;
bool ret;
doc->DispatchEvent(event, &ret);
}
void
TouchCaret::SetState(TouchCaretState aState)
{
TOUCHCARET_LOG("state changed from %d to %d", mState, aState);
if (mState == TOUCHCARET_NONE) {
MOZ_ASSERT(aState != TOUCHCARET_TOUCHDRAG_INACTIVE,
"mState: NONE => TOUCHDRAG_INACTIVE isn't allowed!");
}
if (mState == TOUCHCARET_TOUCHDRAG_ACTIVE) {
MOZ_ASSERT(aState != TOUCHCARET_MOUSEDRAG_ACTIVE,
"mState: TOUCHDRAG_ACTIVE => MOUSEDRAG_ACTIVE isn't allowed!");
}
if (mState == TOUCHCARET_MOUSEDRAG_ACTIVE) {
MOZ_ASSERT(aState == TOUCHCARET_MOUSEDRAG_ACTIVE ||
aState == TOUCHCARET_NONE,
"MOUSEDRAG_ACTIVE allowed next state: NONE!");
}
if (mState == TOUCHCARET_TOUCHDRAG_INACTIVE) {
MOZ_ASSERT(aState == TOUCHCARET_TOUCHDRAG_INACTIVE ||
aState == TOUCHCARET_NONE,
"TOUCHDRAG_INACTIVE allowed next state: NONE!");
}
mState = aState;
if (mState == TOUCHCARET_NONE) {
mActiveTouchId = -1;
mCaretCenterToDownPointOffsetY = 0;
if (mIsValidTap) {
DispatchTapEvent();
mIsValidTap = false;
}
} else if (mState == TOUCHCARET_TOUCHDRAG_ACTIVE ||
mState == TOUCHCARET_MOUSEDRAG_ACTIVE) {
mIsValidTap = true;
}
}