diff --git a/layout/base/SelectionCarets.cpp b/layout/base/SelectionCarets.cpp new file mode 100644 index 000000000000..4677baacdd87 --- /dev/null +++ b/layout/base/SelectionCarets.cpp @@ -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(!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(aIsTilt), err); + + endElement->ClassList()->Toggle(NS_LITERAL_STRING("tilt"), + dom::Optional(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 startNode = + do_QueryInterface(aBackward ? aRange->GetEndParent() : aRange->GetStartParent()); + nsCOMPtr endNode = + do_QueryInterface(aBackward ? aRange->GetStartParent() : aRange->GetEndParent()); + int32_t offset = aBackward ? aRange->EndOffset() : aRange->StartOffset(); + + nsCOMPtr startContent = do_QueryInterface(startNode); + nsCOMPtr 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 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 selection = static_cast(caretSelection); + if (selection->GetRangeCount() <= 0) { + SetVisibility(false); + return; + } + + nsRefPtr 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 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 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 fs = caretFocusFrame->GetFrameSelection(); + fs->SetMouseDownState(true); + nsFrame* frame = static_cast(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 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 selection = static_cast(caretSelection); + if (selection->GetRangeCount() <= 0) { + return nsEventStatus_eConsumeNoDefault; + } + + nsRefPtr range = selection->GetRangeAt(0); + nsIFrame* caretFocusFrame = GetCaretFocusFrame(); + nsRefPtr 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 selection = static_cast(caretSelection); + if (selection->GetRangeCount() <= 0) { + return 0; + } + nsRefPtr range = selection->GetRangeAt(0); + nsRefPtr fs = caretFocusFrame->GetFrameSelection(); + + MOZ_ASSERT(mDragMode != NONE); + nsCOMPtr 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 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 selection = static_cast(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 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 caret = mPresShell->GetCaret(); + NS_ENSURE_TRUE(caret, false); + + bool caretVisible = false; + caret->GetCaretVisible(&caretVisible); + return caretVisible; +} + +nsISelection* +SelectionCarets::GetSelection() +{ + nsRefPtr 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 self = static_cast(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 self = static_cast(aSelectionCarets); + NS_PRECONDITION(aTimer == self->mScrollEndDetectorTimer, + "Unexpected timer"); + self->SetVisibility(true); + self->UpdateSelectionCarets(); +} diff --git a/layout/base/SelectionCarets.h b/layout/base/SelectionCarets.h new file mode 100644 index 000000000000..4f2813441821 --- /dev/null +++ b/layout/base/SelectionCarets.h @@ -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 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 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__ diff --git a/layout/base/TouchCaret.cpp b/layout/base/TouchCaret.cpp index 952caa90ca80..c1bda3d9b2fc 100644 --- a/layout/base/TouchCaret.cpp +++ b/layout/base/TouchCaret.cpp @@ -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 diff --git a/layout/base/TouchCaret.h b/layout/base/TouchCaret.h index 4b3c22435279..0f8f63b02123 100644 --- a/layout/base/TouchCaret.h +++ b/layout/base/TouchCaret.h @@ -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__ diff --git a/layout/base/moz.build b/layout/base/moz.build index 5522d1711639..cd0cc33a4a50 100644 --- a/layout/base/moz.build +++ b/layout/base/moz.build @@ -90,6 +90,7 @@ UNIFIED_SOURCES += [ 'PositionedEventTargeting.cpp', 'RestyleManager.cpp', 'RestyleTracker.cpp', + 'SelectionCarets.cpp', 'StackArena.cpp', 'TouchCaret.cpp', ]