Bug 987718 - Part 5: Add SelectionCarets; r=roc

This commit is contained in:
Morris Tseng 2014-06-04 22:57:00 +02:00
parent 4ef0876bc3
commit 508f746be3
5 changed files with 1104 additions and 3 deletions

View File

@ -0,0 +1,883 @@
/* -*- 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 "SelectionCarets.h"
#include "gfxPrefs.h"
#include "nsBidiPresUtils.h"
#include "nsCanvasFrame.h"
#include "nsCaret.h"
#include "nsContentUtils.h"
#include "nsDebug.h"
#include "nsDOMTokenList.h"
#include "nsFrame.h"
#include "nsIDocument.h"
#include "nsIDocShell.h"
#include "nsIDOMDocument.h"
#include "nsIDOMNodeFilter.h"
#include "nsIPresShell.h"
#include "nsPresContext.h"
#include "nsRect.h"
#include "nsView.h"
#include "mozilla/dom/DOMRect.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/Selection.h"
#include "mozilla/dom/TreeWalker.h"
#include "mozilla/Preferences.h"
#include "mozilla/TouchEvents.h"
#include "TouchCaret.h"
using namespace mozilla;
// We treat mouse/touch move as "REAL" move event once its move distance
// exceed this value, in CSS pixel.
static const int32_t kMoveStartTolerancePx = 5;
// Time for trigger scroll end event, in miliseconds.
static const int32_t kScrollEndTimerDelay = 300;
NS_IMPL_ISUPPORTS(SelectionCarets,
nsISelectionListener,
nsIScrollObserver,
nsISupportsWeakReference)
/*static*/ int32_t SelectionCarets::sSelectionCaretsInflateSize = 0;
SelectionCarets::SelectionCarets(nsIPresShell *aPresShell)
: mActiveTouchId(-1)
, mCaretCenterToDownPointOffsetY(0)
, mDragMode(NONE)
, mVisible(false)
, mStartCaretVisible(false)
, mEndCaretVisible(false)
{
MOZ_ASSERT(NS_IsMainThread());
static bool addedPref = false;
if (!addedPref) {
Preferences::AddIntVarCache(&sSelectionCaretsInflateSize,
"selectioncaret.inflatesize.threshold");
addedPref = true;
}
mPresShell = aPresShell;
}
SelectionCarets::~SelectionCarets()
{
MOZ_ASSERT(NS_IsMainThread());
if (mLongTapDetectorTimer) {
mLongTapDetectorTimer->Cancel();
mLongTapDetectorTimer = nullptr;
}
if (mScrollEndDetectorTimer) {
mScrollEndDetectorTimer->Cancel();
mScrollEndDetectorTimer = nullptr;
}
mPresShell = nullptr;
}
static bool
IsOnRect(const nsRect& aRect,
const nsPoint& aPoint,
int32_t aInflateSize)
{
// Check if the click was in the bounding box of the selection caret
nsRect rect = aRect;
rect.Inflate(aInflateSize);
return rect.Contains(aPoint);
}
nsEventStatus
SelectionCarets::HandleEvent(WidgetEvent* aEvent)
{
WidgetMouseEvent *mouseEvent = aEvent->AsMouseEvent();
if (mouseEvent && mouseEvent->reason == WidgetMouseEvent::eSynthesized) {
return nsEventStatus_eIgnore;
}
WidgetTouchEvent *touchEvent = aEvent->AsTouchEvent();
nsIntPoint movePoint;
int32_t nowTouchId = -1;
if (touchEvent && !touchEvent->touches.IsEmpty()) {
// If touch happened, just grab event with same identifier
if (mActiveTouchId >= 0) {
for (uint32_t i = 0; i < touchEvent->touches.Length(); ++i) {
if (touchEvent->touches[i]->Identifier() == mActiveTouchId) {
movePoint = touchEvent->touches[i]->mRefPoint;
nowTouchId = touchEvent->touches[i]->Identifier();
break;
}
}
// not found, consume it
if (nowTouchId == -1) {
return nsEventStatus_eConsumeNoDefault;
}
} else {
movePoint = touchEvent->touches[0]->mRefPoint;
nowTouchId = touchEvent->touches[0]->Identifier();
}
} else if (mouseEvent) {
movePoint = LayoutDeviceIntPoint::ToUntyped(mouseEvent->AsGUIEvent()->refPoint);
}
// Get event coordinate relative to canvas frame
nsIFrame* canvasFrame = mPresShell->GetCanvasFrame();
if (!canvasFrame) {
return nsEventStatus_eIgnore;
}
nsPoint ptInCanvas =
nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, movePoint, canvasFrame);
if (aEvent->message == NS_TOUCH_START ||
(aEvent->message == NS_MOUSE_BUTTON_DOWN &&
mouseEvent->button == WidgetMouseEvent::eLeftButton)) {
// If having a active touch, ignore other touch down event
if (aEvent->message == NS_TOUCH_START && mActiveTouchId >= 0) {
return nsEventStatus_eConsumeNoDefault;
}
mActiveTouchId = nowTouchId;
mDownPoint = ptInCanvas;
int32_t inflateSize = SelectionCaretsInflateSize();
if (mVisible && IsOnRect(GetStartFrameRect(), ptInCanvas, inflateSize)) {
mDragMode = START_FRAME;
mCaretCenterToDownPointOffsetY = GetCaretYCenterPosition() - ptInCanvas.y;
SetSelectionDirection(false);
SetMouseDownState(true);
return nsEventStatus_eConsumeNoDefault;
} else if (mVisible && IsOnRect(GetEndFrameRect(), ptInCanvas, inflateSize)) {
mDragMode = END_FRAME;
mCaretCenterToDownPointOffsetY = GetCaretYCenterPosition() - ptInCanvas.y;
SetSelectionDirection(true);
SetMouseDownState(true);
return nsEventStatus_eConsumeNoDefault;
} else {
mDragMode = NONE;
mActiveTouchId = -1;
SetVisibility(false);
LaunchLongTapDetector();
}
} else if (aEvent->message == NS_TOUCH_END ||
aEvent->message == NS_TOUCH_CANCEL ||
aEvent->message == NS_MOUSE_BUTTON_UP) {
CancelLongTapDetector();
if (mDragMode != NONE) {
// Only care about same id
if (mActiveTouchId == nowTouchId) {
SetMouseDownState(false);
mDragMode = NONE;
mActiveTouchId = -1;
}
return nsEventStatus_eConsumeNoDefault;
}
} else if (aEvent->message == NS_TOUCH_MOVE ||
aEvent->message == NS_MOUSE_MOVE) {
if (mDragMode == START_FRAME || mDragMode == END_FRAME) {
if (mActiveTouchId == nowTouchId) {
ptInCanvas.y += mCaretCenterToDownPointOffsetY;
return DragSelection(ptInCanvas);
}
return nsEventStatus_eConsumeNoDefault;
}
nsPoint delta = mDownPoint - ptInCanvas;
if (NS_hypot(delta.x, delta.y) >
nsPresContext::AppUnitsPerCSSPixel() * kMoveStartTolerancePx) {
CancelLongTapDetector();
}
} else if (aEvent->message == NS_MOUSE_MOZLONGTAP) {
if (!mVisible) {
SelectWord();
return nsEventStatus_eConsumeNoDefault;
}
}
return nsEventStatus_eIgnore;
}
static void
SetElementVisibility(dom::Element* aElement, bool aVisible)
{
NS_ENSURE_TRUE_VOID(aElement);
ErrorResult err;
aElement->ClassList()->Toggle(NS_LITERAL_STRING("hidden"),
dom::Optional<bool>(!aVisible), err);
}
void
SelectionCarets::SetVisibility(bool aVisible)
{
if (!mPresShell) {
return;
}
if (mVisible == aVisible) {
return;
}
mVisible = aVisible;
dom::Element* startElement = mPresShell->GetSelectionCaretsStartElement();
SetElementVisibility(startElement, mVisible && mStartCaretVisible);
dom::Element* endElement = mPresShell->GetSelectionCaretsEndElement();
SetElementVisibility(endElement, mVisible && mEndCaretVisible);
// We must call SetHasTouchCaret() in order to get APZC to wait until the
// event has been round-tripped and check whether it has been handled,
// otherwise B2G will end up panning the document when the user tries to drag
// selection caret.
mPresShell->SetMayHaveTouchCaret(mVisible);
}
void
SelectionCarets::SetStartFrameVisibility(bool aVisible)
{
mStartCaretVisible = aVisible;
dom::Element* element = mPresShell->GetSelectionCaretsStartElement();
SetElementVisibility(element, mVisible && mStartCaretVisible);
}
void
SelectionCarets::SetEndFrameVisibility(bool aVisible)
{
mEndCaretVisible = aVisible;
dom::Element* element = mPresShell->GetSelectionCaretsEndElement();
SetElementVisibility(element, mVisible && mEndCaretVisible);
}
void
SelectionCarets::SetTilted(bool aIsTilt)
{
dom::Element* startElement = mPresShell->GetSelectionCaretsStartElement();
dom::Element* endElement = mPresShell->GetSelectionCaretsEndElement();
NS_ENSURE_TRUE_VOID(startElement && endElement);
ErrorResult err;
startElement->ClassList()->Toggle(NS_LITERAL_STRING("tilt"),
dom::Optional<bool>(aIsTilt), err);
endElement->ClassList()->Toggle(NS_LITERAL_STRING("tilt"),
dom::Optional<bool>(aIsTilt), err);
}
static void
SetCaretDirection(dom::Element* aElement, bool aIsRight)
{
MOZ_ASSERT(aElement);
ErrorResult err;
if (aIsRight) {
aElement->ClassList()->Add(NS_LITERAL_STRING("moz-selectioncaret-right"), err);
aElement->ClassList()->Remove(NS_LITERAL_STRING("moz-selectioncaret-left"), err);
} else {
aElement->ClassList()->Add(NS_LITERAL_STRING("moz-selectioncaret-left"), err);
aElement->ClassList()->Remove(NS_LITERAL_STRING("moz-selectioncaret-right"), err);
}
}
static bool
IsRightToLeft(nsIFrame* aFrame)
{
MOZ_ASSERT(aFrame);
return aFrame->IsFrameOfType(nsIFrame::eLineParticipant) ?
(nsBidiPresUtils::GetFrameEmbeddingLevel(aFrame) & 1) :
aFrame->StyleVisibility()->mDirection == NS_STYLE_DIRECTION_RTL;
}
/*
* Reduce rect to 1 app unit width along either left or right edge base on
* aToRightEdge parameter.
*/
static void
ReduceRectToVerticalEdge(nsRect& aRect, bool aToRightEdge)
{
if (aToRightEdge) {
aRect.x = aRect.XMost() - 1;
}
aRect.width = 1;
}
static nsIFrame*
FindFirstNodeWithFrame(nsIDocument* aDocument,
nsRange* aRange,
nsFrameSelection* aFrameSelection,
bool aBackward,
int& aOutOffset)
{
NS_ENSURE_TRUE(aDocument && aRange && aFrameSelection, nullptr);
nsCOMPtr<nsINode> startNode =
do_QueryInterface(aBackward ? aRange->GetEndParent() : aRange->GetStartParent());
nsCOMPtr<nsINode> endNode =
do_QueryInterface(aBackward ? aRange->GetStartParent() : aRange->GetEndParent());
int32_t offset = aBackward ? aRange->EndOffset() : aRange->StartOffset();
nsCOMPtr<nsIContent> startContent = do_QueryInterface(startNode);
nsCOMPtr<nsIContent> endContent = do_QueryInterface(endNode);
nsFrameSelection::HINT hintStart =
nsFrameSelection::GetHintForPosition(startContent, offset);
nsIFrame* startFrame = aFrameSelection->GetFrameForNodeOffset(startContent,
offset,
hintStart,
&aOutOffset);
if (startFrame) {
return startFrame;
}
ErrorResult err;
nsRefPtr<dom::TreeWalker> walker =
aDocument->CreateTreeWalker(*startNode,
nsIDOMNodeFilter::SHOW_ALL,
nullptr,
err);
NS_ENSURE_TRUE(walker, nullptr);
startFrame = startContent ? startContent->GetPrimaryFrame() : nullptr;
while (!startFrame && startNode != endNode) {
if (aBackward) {
startNode = walker->PreviousNode(err);
} else {
startNode = walker->NextNode(err);
}
startContent = do_QueryInterface(startNode);
startFrame = startContent ? startContent->GetPrimaryFrame() : nullptr;
}
return startFrame;
}
void
SelectionCarets::UpdateSelectionCarets()
{
if (!mPresShell) {
return;
}
nsISelection* caretSelection = GetSelection();
if (!caretSelection) {
SetVisibility(false);
return;
}
nsRefPtr<dom::Selection> selection = static_cast<dom::Selection*>(caretSelection);
if (selection->GetRangeCount() <= 0) {
SetVisibility(false);
return;
}
nsRefPtr<nsRange> range = selection->GetRangeAt(0);
if (range->Collapsed()) {
SetVisibility(false);
return;
}
nsLayoutUtils::FirstAndLastRectCollector collector;
nsRange::CollectClientRects(&collector, range,
range->GetStartParent(), range->StartOffset(),
range->GetEndParent(), range->EndOffset());
nsIFrame* canvasFrame = mPresShell->GetCanvasFrame();
nsIFrame* rootFrame = mPresShell->GetRootFrame();
if (!canvasFrame || !rootFrame) {
SetVisibility(false);
return;
}
// Check if caret inside the scroll frame's boundary
nsIFrame* caretFocusFrame = GetCaretFocusFrame();
if (!caretFocusFrame) {
SetVisibility(false);
return;
}
nsIContent *editableAncestor = caretFocusFrame->GetContent()->GetEditingHost();
if (!editableAncestor) {
SetVisibility(false);
return;
}
nsRect resultRect;
for (nsIFrame* frame = editableAncestor->GetPrimaryFrame();
frame != nullptr;
frame = frame->GetNextContinuation()) {
nsRect rect = frame->GetRectRelativeToSelf();
nsLayoutUtils::TransformRect(frame, rootFrame, rect);
resultRect = resultRect.Union(rect);
}
// Check start and end frame is rtl or ltr text
nsRefPtr<nsFrameSelection> fs = caretFocusFrame->GetFrameSelection();
int32_t startOffset;
nsIFrame* startFrame = FindFirstNodeWithFrame(mPresShell->GetDocument(),
range, fs, false, startOffset);
int32_t endOffset;
nsIFrame* endFrame = FindFirstNodeWithFrame(mPresShell->GetDocument(),
range, fs, true, endOffset);
if (!startFrame || !endFrame) {
SetVisibility(false);
return;
}
// Check if startFrame is after endFrame.
if (nsLayoutUtils::CompareTreePosition(startFrame, endFrame) > 0) {
SetVisibility(false);
return;
}
bool startFrameIsRTL = IsRightToLeft(startFrame);
bool endFrameIsRTL = IsRightToLeft(endFrame);
// If start frame is LTR, then place start caret in first rect's leftmost
// otherwise put it to first rect's rightmost.
ReduceRectToVerticalEdge(collector.mFirstRect, startFrameIsRTL);
// Contrary to start frame, if end frame is LTR, put end caret to last
// rect's rightmost position, otherwise, put it to last rect's leftmost.
ReduceRectToVerticalEdge(collector.mLastRect, !endFrameIsRTL);
SetStartFrameVisibility(resultRect.Intersects(collector.mFirstRect));
SetEndFrameVisibility(resultRect.Intersects(collector.mLastRect));
nsLayoutUtils::TransformRect(rootFrame, canvasFrame, collector.mFirstRect);
nsLayoutUtils::TransformRect(rootFrame, canvasFrame, collector.mLastRect);
SetStartFramePos(collector.mFirstRect.BottomLeft());
SetEndFramePos(collector.mLastRect.BottomRight());
SetVisibility(true);
// If range select only one character, append tilt class name to it.
bool isTilt = false;
if (startFrame) {
nsPeekOffsetStruct pos(eSelectCluster,
eDirNext,
startOffset,
0,
false,
true, //limit on scrolled views
false,
false);
startFrame->PeekOffset(&pos);
nsCOMPtr<nsIContent> endContent = do_QueryInterface(range->GetEndParent());
if (nsLayoutUtils::CompareTreePosition(pos.mResultContent, endContent) > 0 ||
(pos.mResultContent == endContent &&
pos.mContentOffset >= range->EndOffset())) {
isTilt = true;
}
}
SetCaretDirection(mPresShell->GetSelectionCaretsStartElement(), startFrameIsRTL);
SetCaretDirection(mPresShell->GetSelectionCaretsEndElement(), !endFrameIsRTL);
SetTilted(isTilt);
}
nsresult
SelectionCarets::SelectWord()
{
// If caret isn't visible, the word is not selectable
if (!GetCaretVisible()) {
return NS_OK;
}
NS_ENSURE_TRUE(mPresShell, NS_OK);
nsIFrame* canvasFrame = mPresShell->GetCanvasFrame();
NS_ENSURE_TRUE(canvasFrame, NS_OK);
// Find content offsets for mouse down point
nsIFrame *ptFrame = nsLayoutUtils::GetFrameForPoint(canvasFrame, mDownPoint,
nsLayoutUtils::IGNORE_PAINT_SUPPRESSION | nsLayoutUtils::IGNORE_CROSS_DOC);
NS_ENSURE_TRUE(ptFrame, NS_OK);
nsPoint ptInFrame = mDownPoint;
nsLayoutUtils::TransformPoint(canvasFrame, ptFrame, ptInFrame);
nsIFrame* caretFocusFrame = GetCaretFocusFrame();
nsRefPtr<nsFrameSelection> fs = caretFocusFrame->GetFrameSelection();
fs->SetMouseDownState(true);
nsFrame* frame = static_cast<nsFrame*>(ptFrame);
nsresult rs = frame->SelectByTypeAtPoint(mPresShell->GetPresContext(), ptInFrame,
eSelectWord, eSelectWord, 0);
fs->SetMouseDownState(false);
// Clear maintain selection otherwise we cannot select less than a word
fs->MaintainSelection();
return rs;
}
/*
* If we're dragging start caret, we do not want to drag over previous
* character of end caret. Same as end caret. So we check if content offset
* exceed previous/next character of end/start caret base on aDragMode.
*/
static bool
CompareRangeWithContentOffset(nsRange* aRange,
nsFrameSelection* aSelection,
nsIFrame::ContentOffsets& aOffsets,
SelectionCarets::DragMode aDragMode)
{
MOZ_ASSERT(aDragMode != SelectionCarets::NONE);
nsINode* node = nullptr;
int32_t nodeOffset = 0;
nsFrameSelection::HINT hint = nsFrameSelection::HINTLEFT;
nsDirection dir;
if (aDragMode == SelectionCarets::START_FRAME) {
// Check previous character of end node offset
node = aRange->GetEndParent();
nodeOffset = aRange->EndOffset();
hint = nsFrameSelection::HINTLEFT;
dir = eDirPrevious;
} else {
// Check next character of start node offset
node = aRange->GetStartParent();
nodeOffset = aRange->StartOffset();
hint = nsFrameSelection::HINTRIGHT;
dir = eDirNext;
}
nsCOMPtr<nsIContent> content = do_QueryInterface(node);
int32_t offset = 0;
nsIFrame* theFrame =
aSelection->GetFrameForNodeOffset(content, nodeOffset, hint, &offset);
NS_ENSURE_TRUE(theFrame, false);
// Move one character forward/backward from point and get offset
nsPeekOffsetStruct pos(eSelectCluster,
dir,
offset,
0,
true,
true, //limit on scrolled views
false,
false);
nsresult rv = theFrame->PeekOffset(&pos);
if (NS_FAILED(rv)) {
pos.mResultContent = content;
pos.mContentOffset = nodeOffset;
}
// Compare with current point
int32_t result = nsContentUtils::ComparePoints(aOffsets.content,
aOffsets.StartOffset(),
pos.mResultContent,
pos.mContentOffset);
if ((aDragMode == SelectionCarets::START_FRAME && result == 1) ||
(aDragMode == SelectionCarets::END_FRAME && result == -1)) {
aOffsets.content = pos.mResultContent;
aOffsets.offset = pos.mContentOffset;
aOffsets.secondaryOffset = pos.mContentOffset;
}
return true;
}
nsEventStatus
SelectionCarets::DragSelection(const nsPoint &movePoint)
{
nsIFrame* canvasFrame = mPresShell->GetCanvasFrame();
NS_ENSURE_TRUE(canvasFrame, nsEventStatus_eConsumeNoDefault);
// Find out which content we point to
nsIFrame *ptFrame = nsLayoutUtils::GetFrameForPoint(canvasFrame, movePoint,
nsLayoutUtils::IGNORE_PAINT_SUPPRESSION | nsLayoutUtils::IGNORE_CROSS_DOC);
NS_ENSURE_TRUE(ptFrame, nsEventStatus_eConsumeNoDefault);
nsPoint ptInFrame = movePoint;
nsLayoutUtils::TransformPoint(canvasFrame, ptFrame, ptInFrame);
nsFrame::ContentOffsets offsets =
ptFrame->GetContentOffsetsFromPoint(ptInFrame);
NS_ENSURE_TRUE(offsets.content, nsEventStatus_eConsumeNoDefault);
nsISelection* caretSelection = GetSelection();
nsRefPtr<dom::Selection> selection = static_cast<dom::Selection*>(caretSelection);
if (selection->GetRangeCount() <= 0) {
return nsEventStatus_eConsumeNoDefault;
}
nsRefPtr<nsRange> range = selection->GetRangeAt(0);
nsIFrame* caretFocusFrame = GetCaretFocusFrame();
nsRefPtr<nsFrameSelection> fs = caretFocusFrame->GetFrameSelection();
if (!CompareRangeWithContentOffset(range, fs, offsets, mDragMode)) {
return nsEventStatus_eConsumeNoDefault;
}
// Move caret postion.
nsIFrame *scrollable =
nsLayoutUtils::GetClosestFrameOfType(caretFocusFrame, nsGkAtoms::scrollFrame);
nsWeakFrame weakScrollable = scrollable;
fs->HandleClick(offsets.content, offsets.StartOffset(),
offsets.EndOffset(),
true,
false,
offsets.associateWithNext);
if (!weakScrollable.IsAlive()) {
return nsEventStatus_eConsumeNoDefault;
}
// Scroll scrolled frame.
nsIScrollableFrame *saf = do_QueryFrame(scrollable);
nsIFrame *capturingFrame = saf->GetScrolledFrame();
nsPoint ptInScrolled = movePoint;
nsLayoutUtils::TransformPoint(canvasFrame, capturingFrame, ptInScrolled);
fs->StartAutoScrollTimer(capturingFrame, ptInScrolled, TouchCaret::sAutoScrollTimerDelay);
UpdateSelectionCarets();
return nsEventStatus_eConsumeNoDefault;
}
nscoord
SelectionCarets::GetCaretYCenterPosition()
{
nsIFrame* canvasFrame = mPresShell->GetCanvasFrame();
nsIFrame* caretFocusFrame = GetCaretFocusFrame();
if (!canvasFrame || !caretFocusFrame) {
return 0;
}
nsISelection* caretSelection = GetSelection();
nsRefPtr<dom::Selection> selection = static_cast<dom::Selection*>(caretSelection);
if (selection->GetRangeCount() <= 0) {
return 0;
}
nsRefPtr<nsRange> range = selection->GetRangeAt(0);
nsRefPtr<nsFrameSelection> fs = caretFocusFrame->GetFrameSelection();
MOZ_ASSERT(mDragMode != NONE);
nsCOMPtr<nsIContent> node;
uint32_t nodeOffset;
if (mDragMode == START_FRAME) {
node = do_QueryInterface(range->GetStartParent());
nodeOffset = range->StartOffset();
} else {
node = do_QueryInterface(range->GetEndParent());
nodeOffset = range->EndOffset();
}
int32_t offset;
nsFrameSelection::HINT hint =
nsFrameSelection::GetHintForPosition(node, nodeOffset);
nsIFrame* theFrame =
fs->GetFrameForNodeOffset(node, nodeOffset, hint, &offset);
if (!theFrame) {
return 0;
}
nsRect frameRect = theFrame->GetRectRelativeToSelf();
nsLayoutUtils::TransformRect(theFrame, canvasFrame, frameRect);
return frameRect.Center().y;
}
void
SelectionCarets::SetMouseDownState(bool aState)
{
nsIFrame* caretFocusFrame = GetCaretFocusFrame();
nsRefPtr<nsFrameSelection> fs = caretFocusFrame->GetFrameSelection();
if (fs->GetMouseDownState() == aState) {
return;
}
fs->SetMouseDownState(aState);
if (aState) {
fs->StartBatchChanges();
} else {
fs->EndBatchChanges();
}
}
void
SelectionCarets::SetSelectionDirection(bool aForward)
{
nsISelection* caretSelection = GetSelection();
nsRefPtr<dom::Selection> selection = static_cast<dom::Selection*>(caretSelection);
selection->SetDirection(aForward ? eDirNext : eDirPrevious);
}
static void
SetFramePos(dom::Element* aElement, const nsPoint& aPosition)
{
NS_ENSURE_TRUE_VOID(aElement);
nsAutoString styleStr;
styleStr.AppendLiteral("left:");
styleStr.AppendFloat(nsPresContext::AppUnitsToFloatCSSPixels(aPosition.x));
styleStr.AppendLiteral("px;top:");
styleStr.AppendFloat(nsPresContext::AppUnitsToFloatCSSPixels(aPosition.y));
styleStr.AppendLiteral("px;");
aElement->SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleStr, true);
}
void
SelectionCarets::SetStartFramePos(const nsPoint& aPosition)
{
SetFramePos(mPresShell->GetSelectionCaretsStartElement(), aPosition);
}
void
SelectionCarets::SetEndFramePos(const nsPoint& aPosition)
{
SetFramePos(mPresShell->GetSelectionCaretsEndElement(), aPosition);
}
nsRect
SelectionCarets::GetStartFrameRect()
{
nsIFrame* canvasFrame = mPresShell->GetCanvasFrame();
dom::Element* element = mPresShell->GetSelectionCaretsStartElement();
NS_ENSURE_TRUE(element, nsRect());
nsIFrame* frame = element->GetPrimaryFrame();
NS_ENSURE_TRUE(frame, nsRect());
nsRect frameRect = frame->GetRectRelativeToSelf();
nsLayoutUtils::TransformRect(frame, canvasFrame, frameRect);
return frameRect;
}
nsRect
SelectionCarets::GetEndFrameRect()
{
nsIFrame* canvasFrame = mPresShell->GetCanvasFrame();
dom::Element* element = mPresShell->GetSelectionCaretsEndElement();
NS_ENSURE_TRUE(element, nsRect());
nsIFrame* frame = element->GetPrimaryFrame();
NS_ENSURE_TRUE(frame, nsRect());
nsRect frameRect = frame->GetRectRelativeToSelf();
nsLayoutUtils::TransformRect(frame, canvasFrame, frameRect);
return frameRect;
}
nsIFrame*
SelectionCarets::GetCaretFocusFrame()
{
nsRefPtr<nsCaret> caret = mPresShell->GetCaret();
NS_ENSURE_TRUE(caret, nullptr);
nsISelection* caretSelection = caret->GetCaretDOMSelection();
nsRect focusRect;
return caret->GetGeometry(caretSelection, &focusRect);
}
bool
SelectionCarets::GetCaretVisible()
{
NS_ENSURE_TRUE(mPresShell, false);
nsRefPtr<nsCaret> caret = mPresShell->GetCaret();
NS_ENSURE_TRUE(caret, false);
bool caretVisible = false;
caret->GetCaretVisible(&caretVisible);
return caretVisible;
}
nsISelection*
SelectionCarets::GetSelection()
{
nsRefPtr<nsCaret> caret = mPresShell->GetCaret();
return caret->GetCaretDOMSelection();
}
nsresult
SelectionCarets::NotifySelectionChanged(nsIDOMDocument* aDoc,
nsISelection* aSel,
int16_t aReason)
{
bool isCollapsed;
aSel->GetIsCollapsed(&isCollapsed);
if (isCollapsed) {
SetVisibility(false);
return NS_OK;
}
if (aReason & nsISelectionListener::KEYPRESS_REASON) {
SetVisibility(false);
} else {
UpdateSelectionCarets();
}
return NS_OK;
}
void
SelectionCarets::ScrollPositionChanged()
{
SetVisibility(false);
LaunchScrollEndDetector();
}
void
SelectionCarets::LaunchLongTapDetector()
{
if (XRE_GetProcessType() != GeckoProcessType_Default) {
return;
}
if (!mLongTapDetectorTimer) {
mLongTapDetectorTimer = do_CreateInstance("@mozilla.org/timer;1");
}
MOZ_ASSERT(mLongTapDetectorTimer);
CancelLongTapDetector();
int32_t longTapDelay = gfxPrefs::UiClickHoldContextMenusDelay();
mLongTapDetectorTimer->InitWithFuncCallback(FireLongTap,
this,
longTapDelay,
nsITimer::TYPE_ONE_SHOT);
}
void
SelectionCarets::CancelLongTapDetector()
{
if (XRE_GetProcessType() != GeckoProcessType_Default) {
return;
}
if (!mLongTapDetectorTimer) {
return;
}
mLongTapDetectorTimer->Cancel();
}
/* static */void
SelectionCarets::FireLongTap(nsITimer* aTimer, void* aSelectionCarets)
{
nsRefPtr<SelectionCarets> self = static_cast<SelectionCarets*>(aSelectionCarets);
NS_PRECONDITION(aTimer == self->mLongTapDetectorTimer,
"Unexpected timer");
self->SelectWord();
}
void
SelectionCarets::LaunchScrollEndDetector()
{
if (!mScrollEndDetectorTimer) {
mScrollEndDetectorTimer = do_CreateInstance("@mozilla.org/timer;1");
}
MOZ_ASSERT(mScrollEndDetectorTimer);
mScrollEndDetectorTimer->InitWithFuncCallback(FireScrollEnd,
this,
kScrollEndTimerDelay,
nsITimer::TYPE_ONE_SHOT);
}
/* static */void
SelectionCarets::FireScrollEnd(nsITimer* aTimer, void* aSelectionCarets)
{
nsRefPtr<SelectionCarets> self = static_cast<SelectionCarets*>(aSelectionCarets);
NS_PRECONDITION(aTimer == self->mScrollEndDetectorTimer,
"Unexpected timer");
self->SetVisibility(true);
self->UpdateSelectionCarets();
}

View File

@ -0,0 +1,215 @@
/* -*- 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/. */
#ifndef SelectionCarets_h__
#define SelectionCarets_h__
#include "nsIScrollObserver.h"
#include "nsISelectionListener.h"
#include "nsWeakPtr.h"
#include "nsWeakReference.h"
#include "Units.h"
#include "mozilla/EventForwards.h"
class nsCanvasFrame;
class nsIDocument;
class nsIFrame;
class nsIPresShell;
class nsITimer;
class nsIWidget;
class nsPresContext;
namespace mozilla {
/**
* The SelectionCarets draw a pair of carets when the selection is not
* collapsed, one at each end of the selection.
* SelectionCarets also handle visibility, dragging caret and selecting word
* when long tap event fired.
*
* The DOM structure is 2 div elements for showing start and end caret.
*
* Here is an explanation of the html class names:
* .moz-selectioncaret-left: Indicates start DIV.
* .moz-selectioncaret-right: Indicates end DIV.
* .hidden: This class name is set by SetVisibility,
* SetStartFrameVisibility and SetEndFrameVisibility. Element
* with this class name become hidden.
* .tilt: This class name is set by SetTilted. According to the
* UX spec, when selection contains only one characters, the image of
* caret becomes tilt.
*/
class SelectionCarets MOZ_FINAL : public nsISelectionListener,
public nsIScrollObserver,
public nsSupportsWeakReference
{
public:
/**
* Indicate which part of caret we are dragging at.
*/
enum DragMode {
NONE,
START_FRAME,
END_FRAME
};
explicit SelectionCarets(nsIPresShell *aPresShell);
virtual ~SelectionCarets();
NS_DECL_ISUPPORTS
NS_DECL_NSISELECTIONLISTENER
// nsIScrollObserver
virtual void ScrollPositionChanged() MOZ_OVERRIDE;
void Terminate()
{
mPresShell = nullptr;
}
nsEventStatus HandleEvent(WidgetEvent* aEvent);
/**
* Set visibility for selection caret.
*/
void SetVisibility(bool aVisible);
bool GetVisibility() const
{
return mVisible;
}
/**
* Get from pref "selectioncaret.inflatesize.threshold". This will inflate size of
* caret frame when we checking if user click on caret or not. In app units.
*/
static int32_t SelectionCaretsInflateSize()
{
return sSelectionCaretsInflateSize;
}
private:
SelectionCarets() MOZ_DELETE;
/**
* Update selection caret position base on current selection range.
*/
void UpdateSelectionCarets();
/**
* Select word base on current position, only active when element
* is focused. Triggered by long tap event.
*/
nsresult SelectWord();
/**
* Move selection base on current touch/mouse point
*/
nsEventStatus DragSelection(const nsPoint &movePoint);
/**
* Get the vertical center position of selection caret relative to canvas
* frame.
*/
nscoord GetCaretYCenterPosition();
/**
* Simulate mouse down state when we change the selection range.
* Hence, the selection change event will fire normally.
*/
void SetMouseDownState(bool aState);
void SetSelectionDirection(bool aForward);
/**
* Move start frame of selection caret to given position.
* In app units.
*/
void SetStartFramePos(const nsPoint& aPosition);
/**
* Move end frame of selection caret to given position.
* In app units.
*/
void SetEndFramePos(const nsPoint& aPosition);
/**
* Get rect of selection caret's start frame relative
* to document's canvas frame, in app units.
*/
nsRect GetStartFrameRect();
/**
* Get rect of selection caret's end frame relative
* to document's canvas frame, in app units.
*/
nsRect GetEndFrameRect();
/**
* Set visibility for start part of selection caret, this function
* only affects css property of start frame. So it doesn't change
* mVisible member. When caret overflows element's box we'll hide
* it by calling this function.
*/
void SetStartFrameVisibility(bool aVisible);
/**
* Same as above function but for end frame of selection caret.
*/
void SetEndFrameVisibility(bool aVisible);
/**
* Set tilt class name to start and end frame of selection caret.
*/
void SetTilted(bool aIsTilt);
// Utility function
nsIFrame* GetCaretFocusFrame();
bool GetCaretVisible();
nsISelection* GetSelection();
/**
* Detecting long tap using timer
*/
void LaunchLongTapDetector();
void CancelLongTapDetector();
static void FireLongTap(nsITimer* aTimer, void* aSelectionCarets);
void LaunchScrollEndDetector();
static void FireScrollEnd(nsITimer* aTimer, void* aSelectionCarets);
nsIPresShell* mPresShell;
// This timer is used for detecting long tap fire. If content process
// has APZC, we'll use APZC for long tap detecting. Otherwise, we use this
// timer to detect long tap.
nsCOMPtr<nsITimer> mLongTapDetectorTimer;
// This timer is used for detecting scroll end. We don't have
// scroll end event now, so we will fire this event with a
// const time when we scroll. So when timer triggers, we treat it
// as scroll end event.
nsCOMPtr<nsITimer> mScrollEndDetectorTimer;
// When touch or mouse down, we save the position for detecting
// drag distance
nsPoint mDownPoint;
// For filter multitouch event
int32_t mActiveTouchId;
nscoord mCaretCenterToDownPointOffsetY;
DragMode mDragMode;
bool mVisible;
bool mStartCaretVisible;
bool mEndCaretVisible;
// Preference
static int32_t sSelectionCaretsInflateSize;
};
} // namespace mozilla
#endif //SelectionCarets_h__

View File

@ -38,8 +38,6 @@ using namespace mozilla;
// 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;
// The auto scroll timer's interval in milliseconds.
static const int32_t kAutoScrollTimerDelay = 30;
NS_IMPL_ISUPPORTS(TouchCaret, nsISelectionListener)
@ -284,7 +282,7 @@ TouchCaret::MoveCaret(const nsPoint& movePoint)
offsetToCanvasFrame = nsPoint(0,0);
nsLayoutUtils::TransformPoint(capturingFrame, canvasFrame, offsetToCanvasFrame);
pt = movePoint - offsetToCanvasFrame;
fs->StartAutoScrollTimer(capturingFrame, pt, kAutoScrollTimerDelay);
fs->StartAutoScrollTimer(capturingFrame, pt, sAutoScrollTimerDelay);
}
bool

View File

@ -235,6 +235,10 @@ protected:
// Preference
static int32_t sTouchCaretMaxDistance;
static int32_t sTouchCaretExpirationTime;
// The auto scroll timer's interval in miliseconds.
friend class SelectionCarets;
static const int32_t sAutoScrollTimerDelay = 30;
};
} //namespace mozilla
#endif //mozilla_TouchCaret_h__

View File

@ -90,6 +90,7 @@ UNIFIED_SOURCES += [
'PositionedEventTargeting.cpp',
'RestyleManager.cpp',
'RestyleTracker.cpp',
'SelectionCarets.cpp',
'StackArena.cpp',
'TouchCaret.cpp',
]