/* * Copyright (C) 2010, 2011, 2012, 2013 Research In Motion Limited. All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "config.h" #include "SelectionHandler.h" #include "DOMSupport.h" #include "Document.h" #include "FatFingers.h" #include "FloatQuad.h" #include "FocusController.h" #include "Frame.h" #include "FrameSelection.h" #include "FrameView.h" #include "HitTestResult.h" #include "InputHandler.h" #include "IntRect.h" #include "Page.h" #include "RenderLayer.h" #include "SelectionOverlay.h" #include "TouchEventHandler.h" #include "VisibleUnits.h" #include "WebPageClient.h" #include "WebPage_p.h" #include "htmlediting.h" #include #include #include #include // Note: This generates a lot of logs when dumping rects lists. It will seriously // impact performance. Do not enable this during performance tests. #define SHOWDEBUG_SELECTIONHANDLER 0 #define SHOWDEBUG_SELECTIONHANDLER_TIMING 0 using namespace BlackBerry::Platform; using namespace WebCore; #if SHOWDEBUG_SELECTIONHANDLER #define SelectionLog(severity, format, ...) Platform::logAlways(severity, format, ## __VA_ARGS__) #else #define SelectionLog(severity, format, ...) #endif // SHOWDEBUG_SELECTIONHANDLER #if SHOWDEBUG_SELECTIONHANDLER_TIMING #define SelectionTimingLog(severity, format, ...) Platform::logAlways(severity, format, ## __VA_ARGS__) #else #define SelectionTimingLog(severity, format, ...) #endif // SHOWDEBUG_SELECTIONHANDLER_TIMING namespace BlackBerry { namespace WebKit { SelectionHandler::SelectionHandler(WebPagePrivate* page) : m_webPage(page) , m_selectionActive(false) , m_caretActive(false) , m_lastUpdatedEndPointIsValid(false) , m_didSuppressCaretPositionChangedNotification(false) { } SelectionHandler::~SelectionHandler() { } void SelectionHandler::cancelSelection() { m_selectionActive = false; m_lastSelectionRegion = IntRectRegion(); if (m_webPage->m_selectionOverlay) m_webPage->m_selectionOverlay->hide(); // Notify client with empty selection to ensure the handles are removed if // rendering happened prior to processing on webkit thread m_webPage->m_client->notifySelectionDetailsChanged(SelectionDetails()); m_webPage->updateSelectionScrollView(0); SelectionLog(Platform::LogLevelInfo, "SelectionHandler::cancelSelection"); if (m_webPage->m_inputHandler->isInputMode()) m_webPage->m_inputHandler->cancelSelection(); else m_webPage->focusedOrMainFrame()->selection()->clear(); } BlackBerry::Platform::String SelectionHandler::selectedText() const { return m_webPage->focusedOrMainFrame()->editor().selectedText(); } WebCore::IntRect SelectionHandler::clippingRectForVisibleContent() const { // Get the containing content rect for the frame. Frame* frame = m_webPage->focusedOrMainFrame(); WebCore::IntRect clipRect = WebCore::IntRect(WebCore::IntPoint(0, 0), frame->view()->contentsSize()); if (frame != m_webPage->mainFrame()) { clipRect = m_webPage->getRecursiveVisibleWindowRect(frame->view(), true /* no clip to main frame window */); clipRect = m_webPage->m_mainFrame->view()->windowToContents(clipRect); } // Get the input field containing box. WebCore::IntRect inputBoundingBox = m_webPage->m_inputHandler->boundingBoxForInputField(); if (!inputBoundingBox.isEmpty()) { // Adjust the bounding box to the frame offset. inputBoundingBox = m_webPage->mainFrame()->view()->windowToContents(frame->view()->contentsToWindow(inputBoundingBox)); clipRect.intersect(inputBoundingBox); } return clipRect; } void SelectionHandler::regionForTextQuads(Vector &quadList, IntRectRegion& region, bool shouldClipToVisibleContent) const { ASSERT(region.isEmpty()); if (!quadList.isEmpty()) { FrameView* frameView = m_webPage->focusedOrMainFrame()->view(); // frameRect is in frame coordinates. WebCore::IntRect frameRect(WebCore::IntPoint(0, 0), frameView->contentsSize()); // framePosition is in main frame coordinates. WebCore::IntPoint framePosition = m_webPage->frameOffset(m_webPage->focusedOrMainFrame()); // Get the visibile content rect. WebCore::IntRect clippingRect = shouldClipToVisibleContent ? clippingRectForVisibleContent() : WebCore::IntRect(-1, -1, 0, 0); // Convert the text quads into a more platform friendy // IntRectRegion and adjust for subframes. Platform::IntRect selectionBoundingBox; std::vector adjustedIntRects; for (unsigned i = 0; i < quadList.size(); i++) { WebCore::IntRect enclosingRect = quadList[i].enclosingBoundingBox(); enclosingRect.intersect(frameRect); enclosingRect.move(framePosition.x(), framePosition.y()); // Clip to the visible content. if (clippingRect.location() != DOMSupport::InvalidPoint) enclosingRect.intersect(clippingRect); adjustedIntRects.push_back(enclosingRect); selectionBoundingBox.unite(enclosingRect); } region = IntRectRegion(selectionBoundingBox, adjustedIntRects.size(), adjustedIntRects); } } static VisiblePosition visiblePositionForPointIgnoringClipping(const Frame& frame, const WebCore::IntPoint& framePoint) { // Frame::visiblePositionAtPoint hard-codes ignoreClipping=false in the // call to hitTestResultAtPoint. This has a bug where some pages (such as // metafilter) will return the wrong VisiblePosition for points that are // outside the visible rect. To work around the bug, this is a copy of // visiblePositionAtPoint which which passes ignoreClipping=true. // See RIM Bug #4315. HitTestResult result = frame.eventHandler()->hitTestResultAtPoint(framePoint, HitTestRequest::ReadOnly | HitTestRequest::Active | HitTestRequest::IgnoreClipping); Node* node = result.innerNode(); if (!node || node->document() != frame.document()) return VisiblePosition(); RenderObject* renderer = node->renderer(); if (!renderer) return VisiblePosition(); VisiblePosition visiblePos = renderer->positionForPoint(result.localPoint()); if (visiblePos.isNull()) visiblePos = VisiblePosition(Position(createLegacyEditingPosition(node, 0))); return visiblePos; } static unsigned directionOfPointRelativeToRect(const WebCore::IntPoint& point, const WebCore::IntRect& rect, const bool useTopPadding = true, const bool useBottomPadding = true) { ASSERT(!rect.contains(point)); // Padding to prevent accidental trigger of up/down when intending to do horizontal movement. const int verticalPadding = 5; // Do height movement check first but add padding. We may be off on both x & y axis and only // want to move in one direction at a time. if (point.y() - (useTopPadding ? verticalPadding : 0) < rect.y()) return KEYCODE_UP; if (point.y() > rect.maxY() + (useBottomPadding ? verticalPadding : 0)) return KEYCODE_DOWN; if (point.x() < rect.location().x()) return KEYCODE_LEFT; if (point.x() > rect.maxX()) return KEYCODE_RIGHT; return 0; } bool SelectionHandler::shouldUpdateSelectionOrCaretForPoint(const WebCore::IntPoint& point, const WebCore::IntRect& caretRect, bool startCaret) const { ASSERT(m_webPage->m_inputHandler->isInputMode()); // If the point isn't valid don't block change as it is not actually changing. if (point == DOMSupport::InvalidPoint) return true; VisibleSelection currentSelection = m_webPage->focusedOrMainFrame()->selection()->selection(); // If the input field is single line or we are on the first or last // line of a multiline input field only horizontal movement is supported. bool aboveCaret = point.y() < caretRect.y(); bool belowCaret = point.y() >= caretRect.maxY(); SelectionLog(Platform::LogLevelInfo, "SelectionHandler::shouldUpdateSelectionOrCaretForPoint multiline = %s above = %s below = %s first line = %s last line = %s start = %s", m_webPage->m_inputHandler->isMultilineInputMode() ? "true" : "false", aboveCaret ? "true" : "false", belowCaret ? "true" : "false", inSameLine(currentSelection.visibleStart(), startOfEditableContent(currentSelection.visibleStart())) ? "true" : "false", inSameLine(currentSelection.visibleEnd(), endOfEditableContent(currentSelection.visibleEnd())) ? "true" : "false", startCaret ? "true" : "false"); if (!m_webPage->m_inputHandler->isMultilineInputMode() && (aboveCaret || belowCaret)) return false; if (startCaret && inSameLine(currentSelection.visibleStart(), startOfEditableContent(currentSelection.visibleStart())) && aboveCaret) return false; if (!startCaret && inSameLine(currentSelection.visibleEnd(), endOfEditableContent(currentSelection.visibleEnd())) && belowCaret) return false; return true; } void SelectionHandler::setCaretPosition(const WebCore::IntPoint& position) { if (!m_webPage->m_inputHandler->isInputMode() || !m_webPage->focusedOrMainFrame()->document()->focusedElement()) return; m_caretActive = true; SelectionLog(Platform::LogLevelInfo, "SelectionHandler::setCaretPosition requested point %s", Platform::IntPoint(position).toString().c_str()); Frame* focusedFrame = m_webPage->focusedOrMainFrame(); FrameSelection* controller = focusedFrame->selection(); WebCore::IntPoint relativePoint = DOMSupport::convertPointToFrame(m_webPage->mainFrame(), focusedFrame, position); WebCore::IntRect currentCaretRect = controller->selection().visibleStart().absoluteCaretBounds(); if (relativePoint == DOMSupport::InvalidPoint || !shouldUpdateSelectionOrCaretForPoint(relativePoint, currentCaretRect)) { selectionPositionChanged(true /* forceUpdateWithoutChange */); return; } WebCore::IntRect nodeOutlineBounds(m_webPage->m_inputHandler->boundingBoxForInputField()); if (!nodeOutlineBounds.isEmpty() && !nodeOutlineBounds.contains(relativePoint)) { if (unsigned character = directionOfPointRelativeToRect(relativePoint, currentCaretRect)) m_webPage->m_inputHandler->handleKeyboardInput(Platform::KeyboardEvent(character)); // Send the selection changed in case this does not trigger a selection change to // ensure the caret position is accurate. This may be a duplicate event. selectionPositionChanged(true /* forceUpdateWithoutChange */); return; } VisibleSelection newSelection(focusedFrame->visiblePositionForPoint(relativePoint)); if (controller->selection() == newSelection) { selectionPositionChanged(true /* forceUpdateWithoutChange */); return; } controller->setSelection(newSelection); SelectionLog(Platform::LogLevelInfo, "SelectionHandler::setCaretPosition point valid, cursor updated"); } void SelectionHandler::inputHandlerDidFinishProcessingChange() { if (m_didSuppressCaretPositionChangedNotification) notifyCaretPositionChangedIfNeeded(false); } // This function makes sure we are not reducing the selection to a caret selection. static bool shouldExtendSelectionInDirection(const VisibleSelection& selection, unsigned character) { FrameSelection tempSelection; tempSelection.setSelection(selection); switch (character) { case KEYCODE_LEFT: tempSelection.modify(FrameSelection::AlterationExtend, DirectionLeft, CharacterGranularity); break; case KEYCODE_RIGHT: tempSelection.modify(FrameSelection::AlterationExtend, DirectionRight, CharacterGranularity); break; case KEYCODE_UP: tempSelection.modify(FrameSelection::AlterationExtend, DirectionBackward, LineGranularity); break; case KEYCODE_DOWN: tempSelection.modify(FrameSelection::AlterationExtend, DirectionForward, LineGranularity); break; default: break; } if ((character == KEYCODE_LEFT || character == KEYCODE_RIGHT) && (!inSameLine(selection.visibleStart(), tempSelection.selection().visibleStart()) || !inSameLine(selection.visibleEnd(), tempSelection.selection().visibleEnd()))) return false; return tempSelection.selection().selectionType() == VisibleSelection::RangeSelection; } static int clamp(const int min, const int value, const int max) { return value < min ? min : std::min(value, max); } static VisiblePosition directionalVisiblePositionAtExtentOfBox(Frame* frame, const WebCore::IntRect& boundingBox, unsigned direction, const WebCore::IntPoint& basePoint) { ASSERT(frame); if (!frame) return VisiblePosition(); switch (direction) { case KEYCODE_LEFT: // Extend x to start and clamp y to the edge of bounding box. return frame->visiblePositionForPoint(WebCore::IntPoint(boundingBox.x(), clamp(boundingBox.y(), basePoint.y(), boundingBox.maxY()))); case KEYCODE_RIGHT: // Extend x to end and clamp y to the edge of bounding box. return frame->visiblePositionForPoint(WebCore::IntPoint(boundingBox.maxX(), clamp(boundingBox.y(), basePoint.y(), boundingBox.maxY()))); case KEYCODE_UP: // Extend y to top and clamp x to the edge of bounding box. return frame->visiblePositionForPoint(WebCore::IntPoint(clamp(boundingBox.x(), basePoint.x(), boundingBox.maxX()), boundingBox.y())); case KEYCODE_DOWN: // Extend y to bottom and clamp x to the edge of bounding box. return frame->visiblePositionForPoint(WebCore::IntPoint(clamp(boundingBox.x(), basePoint.x(), boundingBox.maxX()), boundingBox.maxY())); default: break; } return frame->visiblePositionForPoint(WebCore::IntPoint(basePoint.x(), basePoint.y())); } static bool pointIsOutsideOfBoundingBoxInDirection(unsigned direction, const WebCore::IntPoint& selectionPoint, const WebCore::IntRect& boundingBox) { if ((direction == KEYCODE_LEFT && selectionPoint.x() < boundingBox.x()) || (direction == KEYCODE_UP && selectionPoint.y() < boundingBox.y()) || (direction == KEYCODE_RIGHT && selectionPoint.x() > boundingBox.maxX()) || (direction == KEYCODE_DOWN && selectionPoint.y() > boundingBox.maxY())) return true; return false; } unsigned SelectionHandler::extendSelectionToFieldBoundary(bool isStartHandle, const WebCore::IntPoint& selectionPoint, VisibleSelection& newSelection) { Frame* focusedFrame = m_webPage->focusedOrMainFrame(); if (!focusedFrame->document()->focusedElement() || !focusedFrame->document()->focusedElement()->renderer()) return 0; VisibleSelection activeSelection = focusedFrame->selection()->selection(); WebCore::IntRect caretRect = isStartHandle ? activeSelection.visibleStart().absoluteCaretBounds() : activeSelection.visibleEnd().absoluteCaretBounds(); WebCore::IntRect nodeBoundingBox = focusedFrame->document()->focusedElement()->renderer()->absoluteBoundingBoxRect(); nodeBoundingBox.inflate(-1); // Start handle is outside of the field. Treat it as the changed handle and move // relative to the start caret rect. unsigned character = directionOfPointRelativeToRect(selectionPoint, caretRect, isStartHandle /* useTopPadding */, !isStartHandle /* useBottomPadding */); // Prevent incorrect movement, handles can only extend the selection this way // to prevent inversion of the handles. if ((isStartHandle && (character == KEYCODE_RIGHT || character == KEYCODE_DOWN)) || (!isStartHandle && (character == KEYCODE_LEFT || character == KEYCODE_UP))) character = 0; VisiblePosition newVisiblePosition = isStartHandle ? activeSelection.extent() : activeSelection.base(); // Extend the selection to the bounds of the box before doing incremental scroll if the point is outside the node. // Don't extend selection and handle the character at the same time. if (pointIsOutsideOfBoundingBoxInDirection(character, selectionPoint, nodeBoundingBox)) newVisiblePosition = directionalVisiblePositionAtExtentOfBox(focusedFrame, nodeBoundingBox, character, selectionPoint); if (isStartHandle) newSelection = VisibleSelection(newVisiblePosition, newSelection.extent(), true /* isDirectional */); else newSelection = VisibleSelection(newSelection.base(), newVisiblePosition, true /* isDirectional */); // If no selection will be changed, return the character to extend using navigation. if (activeSelection == newSelection) return character; // Selection has been updated. return 0; } // Returns true if handled. bool SelectionHandler::updateOrHandleInputSelection(VisibleSelection& newSelection, const WebCore::IntPoint& relativeStart, const WebCore::IntPoint& relativeEnd) { ASSERT(m_webPage->m_inputHandler->isInputMode()); Frame* focusedFrame = m_webPage->focusedOrMainFrame(); Node* focusedNode = focusedFrame->document()->focusedElement(); if (!focusedNode || !focusedNode->renderer()) return false; FrameSelection* controller = focusedFrame->selection(); WebCore::IntRect currentStartCaretRect = controller->selection().visibleStart().absoluteCaretBounds(); WebCore::IntRect currentEndCaretRect = controller->selection().visibleEnd().absoluteCaretBounds(); // Check if the handle movement is valid. if (!shouldUpdateSelectionOrCaretForPoint(relativeStart, currentStartCaretRect, true /* startCaret */) || !shouldUpdateSelectionOrCaretForPoint(relativeEnd, currentEndCaretRect, false /* startCaret */)) { selectionPositionChanged(true /* forceUpdateWithoutChange */); return true; } WebCore::IntRect nodeBoundingBox = focusedNode->renderer()->absoluteBoundingBoxRect(); // Only do special handling if one handle is outside of the node. bool startIsOutsideOfField = relativeStart != DOMSupport::InvalidPoint && !nodeBoundingBox.contains(relativeStart); bool endIsOutsideOfField = relativeEnd != DOMSupport::InvalidPoint && !nodeBoundingBox.contains(relativeEnd); if (startIsOutsideOfField && endIsOutsideOfField) return false; unsigned character = 0; bool needToInvertDirection = false; if (startIsOutsideOfField) { character = extendSelectionToFieldBoundary(true /* isStartHandle */, relativeStart, newSelection); if (character && controller->selection().isBaseFirst()) { // Invert the selection so that the cursor point is at the beginning. controller->setSelection(VisibleSelection(controller->selection().end(), controller->selection().start(), true /* isDirectional */)); needToInvertDirection = true; } } else if (endIsOutsideOfField) { character = extendSelectionToFieldBoundary(false /* isStartHandle */, relativeEnd, newSelection); if (character && !controller->selection().isBaseFirst()) { // Reset the selection so that the end is the edit point. controller->setSelection(VisibleSelection(controller->selection().start(), controller->selection().end(), true /* isDirectional */)); } } if (!character) return false; SelectionLog(Platform::LogLevelInfo, "SelectionHandler::updateOrHandleInputSelection making selection change attempt using key event %d", character); if (shouldExtendSelectionInDirection(controller->selection(), character)) m_webPage->m_inputHandler->handleKeyboardInput(Platform::KeyboardEvent(character, Platform::KeyboardEvent::KeyDown, KEYMOD_SHIFT)); if (needToInvertDirection) controller->setSelection(VisibleSelection(controller->selection().extent(), controller->selection().base(), true /* isDirectional */)); // Send the selection changed in case this does not trigger a selection change to // ensure the caret position is accurate. This may be a duplicate event. selectionPositionChanged(true /* forceUpdateWithoutChange */); return true; } void SelectionHandler::setSelection(WebCore::IntPoint start, WebCore::IntPoint end) { m_selectionActive = true; ASSERT(m_webPage); ASSERT(m_webPage->focusedOrMainFrame()); ASSERT(m_webPage->focusedOrMainFrame()->selection()); Frame* focusedFrame = m_webPage->focusedOrMainFrame(); FrameSelection* controller = focusedFrame->selection(); #if SHOWDEBUG_SELECTIONHANDLER_TIMING m_timer.start(); #endif SelectionLog(Platform::LogLevelInfo, "SelectionHandler::setSelection adjusted points %s, %s", Platform::IntPoint(start).toString().c_str(), Platform::IntPoint(end).toString().c_str()); // Note that IntPoint(-1, -1) is being our sentinel so far for // clipped out selection starting or ending location. bool startIsValid = start != DOMSupport::InvalidPoint; m_lastUpdatedEndPointIsValid = end != DOMSupport::InvalidPoint; // At least one of the locations must be valid. ASSERT(startIsValid || m_lastUpdatedEndPointIsValid); if (m_webPage->m_inputHandler->isInputMode() && !m_webPage->m_inputHandler->isMultilineInputMode()) { WebCore::IntRect caret(startCaretViewportRect(m_webPage->frameOffset(focusedFrame))); if (!caret.isEmpty()) { int centerOfCaretY = caret.center().y(); if (startIsValid) start.setY(centerOfCaretY); if (m_lastUpdatedEndPointIsValid) end.setY(centerOfCaretY); } } WebCore::IntPoint relativeStart = start; WebCore::IntPoint relativeEnd = end; // Initialize the new start and end of our selection at the current positions. VisiblePosition newStart = controller->selection().visibleStart(); VisiblePosition newEnd = controller->selection().visibleEnd(); // We don't return early in the following, so that we can do input field scrolling if the // handle is outside the bounds of the field. This can be extended to handle sub-region // scrolling as well if (startIsValid) { relativeStart = DOMSupport::convertPointToFrame(m_webPage->mainFrame(), focusedFrame, start); VisiblePosition base = visiblePositionForPointIgnoringClipping(*focusedFrame, clipPointToVisibleContainer(start)); if (base.isNotNull()) newStart = base; } if (m_lastUpdatedEndPointIsValid) { relativeEnd = DOMSupport::convertPointToFrame(m_webPage->mainFrame(), focusedFrame, end); VisiblePosition extent = visiblePositionForPointIgnoringClipping(*focusedFrame, clipPointToVisibleContainer(end)); if (extent.isNotNull()) newEnd = extent; } VisibleSelection newSelection(newStart, newEnd, true /* isDirectional */); if (!controller->selection().isRange()) m_webPage->updateSelectionScrollView(newSelection.visibleEnd().deepEquivalent().anchorNode()); if (m_webPage->m_inputHandler->isInputMode()) { if (updateOrHandleInputSelection(newSelection, relativeStart, relativeEnd)) return; } if (controller->selection() == newSelection) { selectionPositionChanged(true /* forceUpdateWithoutChange */); return; } // If the selection size is reduce to less than a character, selection type becomes // Caret. As long as it is still a range, it's a valid selection. Selection cannot // be cancelled through this function. Vector quads; DOMSupport::visibleTextQuads(newSelection, quads); IntRectRegion unclippedRegion; regionForTextQuads(quads, unclippedRegion, false /* shouldClipToVisibleContent */); if (unclippedRegion.isEmpty()) { // Requested selection results in an empty selection, skip this change. selectionPositionChanged(true /* forceUpdateWithoutChange */); SelectionLog(Platform::LogLevelWarn, "SelectionHandler::setSelection selection points invalid, selection not updated."); return; } // Check if the handles reversed position. if (m_selectionActive && !newSelection.isBaseFirst()) { m_webPage->m_client->notifySelectionHandlesReversed(); newSelection = VisibleSelection(newSelection.extent(), newSelection.base()); } controller->setSelection(newSelection); SelectionLog(Platform::LogLevelInfo, "SelectionHandler::setSelection selection points valid, selection updated."); } // FIXME re-use this in context. Must be updated to include an option to return the href. // This function should be moved to a new unit file. Names suggetions include DOMQueries // and NodeTypes. Functions currently in InputHandler.cpp, SelectionHandler.cpp and WebPage.cpp // can all be moved in. static Node* enclosingLinkEventParentForNode(Node* node) { if (!node) return 0; Node* linkNode = node->enclosingLinkEventParentOrSelf(); return linkNode && linkNode->isLink() ? linkNode : 0; } TextGranularity textGranularityFromSelectionExpansionType(SelectionExpansionType selectionExpansionType) { TextGranularity granularity; switch (selectionExpansionType) { case Word: default: granularity = WordGranularity; break; case Sentence: granularity = SentenceGranularity; break; case Paragraph: granularity = ParagraphGranularity; break; } return granularity; } bool SelectionHandler::selectNodeIfFatFingersResultIsLink(FatFingersResult fatFingersResult) { if (!fatFingersResult.isValid()) return false; Node* targetNode = fatFingersResult.node(FatFingersResult::ShadowContentNotAllowed); ASSERT(targetNode); // If the node at the point is a link, focus on the entire link, not a word. if (Node* link = enclosingLinkEventParentForNode(targetNode)) { Element* element = fatFingersResult.nodeAsElementIfApplicable(); if (!element) return false; m_animationHighlightColor = element->renderStyle()->initialTapHighlightColor(); selectObject(link); // If selected object is a link, no need to wait for further expansion. m_webPage->m_client->stopExpandingSelection(); return true; } return false; } WebCore::IntRect SelectionHandler::startCaretViewportRect(const WebCore::IntPoint& frameOffset) const { WebCore::IntRect caretRect; Frame* frame = m_webPage->focusedOrMainFrame(); if (!frame) return caretRect; if (frame->selection()->selectionType() != VisibleSelection::NoSelection) { caretRect = frame->selection()->selection().visibleStart().absoluteCaretBounds(); caretRect.moveBy(frameOffset); } return caretRect; } void SelectionHandler::selectAtPoint(const WebCore::IntPoint& location, SelectionExpansionType selectionExpansionType) { if (selectionExpansionType == Word) { m_animationOverlayStartPos = VisiblePosition(); m_animationOverlayEndPos = VisiblePosition(); m_currentAnimationOverlayRegion = IntRectRegion(); m_nextAnimationOverlayRegion = IntRectRegion(); m_selectionSubframeViewportRect = WebCore::IntRect(); } // If point is invalid trigger selection based expansion. if (location == DOMSupport::InvalidPoint) { selectObject(WordGranularity); return; } WebCore::IntPoint targetPosition; FatFingersResult fatFingersResult = m_webPage->m_touchEventHandler->lastFatFingersResult(); if (selectNodeIfFatFingersResultIsLink(fatFingersResult)) return; if (!fatFingersResult.resultMatches(location, FatFingers::Text) || !fatFingersResult.positionWasAdjusted() || !fatFingersResult.nodeAsElementIfApplicable()) { // Cache text result for later use. fatFingersResult = FatFingers(m_webPage, location, FatFingers::Text).findBestPoint(); m_webPage->m_touchEventHandler->cacheTextResult(fatFingersResult); } if (!fatFingersResult.positionWasAdjusted()) { if (isSelectionActive()) cancelSelection(); m_webPage->m_client->notifySelectionDetailsChanged(SelectionDetails()); m_webPage->m_touchEventHandler->sendClickAtFatFingersPoint(); return; } targetPosition = fatFingersResult.adjustedPosition(); if (selectNodeIfFatFingersResultIsLink(fatFingersResult)) return; selectObject(targetPosition, textGranularityFromSelectionExpansionType(selectionExpansionType)); } static bool isInvalidParagraph(const VisiblePosition& pos) { return endOfParagraph(pos).isNull() || pos == endOfParagraph(pos); } void SelectionHandler::selectNextParagraph() { FrameSelection* controller = m_webPage->focusedOrMainFrame()->selection(); VisiblePosition startPos = VisiblePosition(controller->start(), controller->affinity()); if (isStartOfLine(startPos) && isEndOfDocument(startPos)) startPos = startPos.previous(CannotCrossEditingBoundary); // Find next paragraph end position. VisiblePosition endPos(controller->end(), controller->affinity()); // endPos here indicates the end of current paragraph endPos = endPos.next(CannotCrossEditingBoundary); // find the start of next paragraph while (!isEndOfDocument(endPos) && endPos.isNotNull() && isInvalidParagraph(endPos)) endPos = endPos.next(CannotCrossEditingBoundary); // go to next position endPos = endOfParagraph(endPos); // find the end of paragraph // Set selection if the paragraph is covered by overlay and endPos is not null. if (m_currentAnimationOverlayRegion.extents().bottom() >= endPos.absoluteCaretBounds().maxY() && endPos.isNotNull()) { VisibleSelection selection = VisibleSelection(startPos, endPos); selection.setAffinity(controller->affinity()); controller->setSelection(selection); // Stop expansion if reaching the end of page. if (isEndOfDocument(endPos)) m_webPage->m_client->stopExpandingSelection(); } } void SelectionHandler::drawAnimationOverlay(IntRectRegion overlayRegion, bool isExpandingOverlayAtConstantRate, bool isStartOfSelection) { if (isExpandingOverlayAtConstantRate) { // When overlay expands at a constant rate, the current overlay height increases // m_overlayExpansionHeight each time and the width is always same as next overlay region. WebCore::IntRect currentOverlayRect = m_currentAnimationOverlayRegion.extents(); WebCore::IntRect nextOverlayRect = m_nextAnimationOverlayRegion.extents(); WebCore::IntRect overlayRect(WebCore::IntRect(nextOverlayRect.location(), WebCore::IntSize(nextOverlayRect.width(), currentOverlayRect.height() + m_overlayExpansionHeight))); overlayRegion = IntRectRegion(overlayRect); } m_webPage->m_selectionHighlight->draw(overlayRegion, m_animationHighlightColor.red(), m_animationHighlightColor.green(), m_animationHighlightColor.blue(), m_animationHighlightColor.alpha(), false /* do not hide after scroll */, isStartOfSelection); m_currentAnimationOverlayRegion = overlayRegion; } IntRectRegion SelectionHandler::regionForSelectionQuads(VisibleSelection selection) { Vector quads; DOMSupport::visibleTextQuads(selection, quads); IntRectRegion region; regionForTextQuads(quads, region); return region; } bool SelectionHandler::findNextAnimationOverlayRegion() { // If overlay is at the end of document, stop overlay expansion. if (isEndOfDocument(m_animationOverlayEndPos) || m_animationOverlayEndPos.isNull()) return false; m_animationOverlayEndPos = m_animationOverlayEndPos.next(CannotCrossEditingBoundary); while (!isEndOfDocument(m_animationOverlayEndPos) && m_animationOverlayEndPos.isNotNull() && isInvalidParagraph(m_animationOverlayEndPos)) m_animationOverlayEndPos = m_animationOverlayEndPos.next(CannotCrossEditingBoundary); // go to next position m_animationOverlayEndPos = endOfParagraph(m_animationOverlayEndPos); // find end of paragraph VisibleSelection selection(m_animationOverlayStartPos, m_animationOverlayEndPos); m_nextAnimationOverlayRegion = regionForSelectionQuads(selection); return true; } void SelectionHandler::expandSelection(bool isScrollStarted) { if (m_currentAnimationOverlayRegion.isEmpty() || m_nextAnimationOverlayRegion.isEmpty()) return; WebCore::IntPoint nextOverlayBottomRightPoint = WebCore::IntPoint(m_currentAnimationOverlayRegion.extents().bottomRight()) + WebCore::IntPoint(0, m_overlayExpansionHeight); if (nextOverlayBottomRightPoint.y() > m_nextAnimationOverlayRegion.extents().bottom()) // Find next overlay region so that we can update overlay region's width while expanding. if (!findNextAnimationOverlayRegion()) { drawAnimationOverlay(m_nextAnimationOverlayRegion, false); selectNextParagraph(); return; } // Draw overlay if the position is in the viewport and is not null. // Otherwise, start scrolling if it hasn't started. if (ensureSelectedTextVisible(nextOverlayBottomRightPoint, false /* do not scroll */) && m_animationOverlayEndPos.isNotNull()) drawAnimationOverlay(IntRectRegion(), true /* isExpandingOverlayAtConstantRate */); else if (!isScrollStarted) { m_webPage->m_client->startSelectionScroll(); return; } selectNextParagraph(); } bool SelectionHandler::ensureSelectedTextVisible(const WebCore::IntPoint& point, bool scrollIfNeeded) { WebCore::IntRect viewportRect = selectionViewportRect(); if (!scrollIfNeeded) // If reaching the bottom of content, ignore scroll margin so the text on the bottom can be selected. return viewportRect.maxY() >= m_webPage->contentsSize().height() ? viewportRect.maxY() >= point.y() : viewportRect.maxY() >= point.y() + m_scrollMargin.height(); // Scroll position adjustment here is based on main frame. If selecting in a subframe, don't do animation. if (!m_selectionSubframeViewportRect.isEmpty()) return false; WebCore::IntRect endLocation = m_animationOverlayEndPos.absoluteCaretBounds(); Frame* focusedFrame = m_webPage->focusedOrMainFrame(); Frame* mainFrame = m_webPage->mainFrame(); // If we are selecting within an iframe, translate coordinates to main frame. if (focusedFrame && focusedFrame->view() && mainFrame && mainFrame->view() && focusedFrame != mainFrame) endLocation = mainFrame->view()->windowToContents(focusedFrame->view()->contentsToWindow(endLocation)); Node* anchorNode = m_animationOverlayEndPos.deepEquivalent().anchorNode(); if (!anchorNode || !anchorNode->renderer()) return false; RenderLayer* layer = anchorNode->renderer()->enclosingLayer(); if (!layer) return false; endLocation.inflateX(m_scrollMargin.width()); endLocation.inflateY(m_scrollMargin.height()); WebCore::IntRect revealRect(layer->getRectToExpose(viewportRect, endLocation, ScrollAlignment::alignToEdgeIfNeeded, ScrollAlignment::alignToEdgeIfNeeded)); revealRect.setX(std::min(std::max(revealRect.x(), 0), m_webPage->maximumScrollPosition().x())); revealRect.setY(std::min(std::max(revealRect.y(), 0), m_webPage->maximumScrollPosition().y())); // Animate scroll position to revealRect. m_webPage->animateToScaleAndDocumentScrollPosition(m_webPage->currentScale() /* Don't zoom */, WebCore::FloatPoint(revealRect.x(), revealRect.y())); return true; } WebCore::IntRect SelectionHandler::selectionViewportRect() const { if (m_selectionSubframeViewportRect.isEmpty()) return WebCore::IntRect(m_webPage->scrollPosition(), m_selectionViewportSize); return m_selectionSubframeViewportRect; } void SelectionHandler::setParagraphExpansionScrollMargin(const WebCore::IntSize& scrollMargin) { m_scrollMargin.setWidth(scrollMargin.width()); m_scrollMargin.setHeight(scrollMargin.height()); } bool SelectionHandler::expandSelectionToGranularity(Frame* frame, VisibleSelection selection, TextGranularity granularity, bool isInputMode) { ASSERT(frame); ASSERT(frame->selection()); if (!(selection.start().anchorNode() && selection.start().anchorNode()->isTextNode())) return false; if (granularity == WordGranularity) selection = DOMSupport::visibleSelectionForClosestActualWordStart(selection); selection.expandUsingGranularity(granularity); selection.setAffinity(frame->selection()->affinity()); if (isInputMode && !frame->selection()->shouldChangeSelection(selection)) return false; m_animationOverlayStartPos = selection.visibleStart(); m_animationOverlayEndPos = selection.visibleEnd(); if (granularity == WordGranularity) { m_webPage->updateSelectionScrollView(selection.visibleEnd().deepEquivalent().anchorNode()); Element* element = m_animationOverlayStartPos.deepEquivalent().element(); if (!element) return false; m_animationHighlightColor = element->renderStyle()->initialTapHighlightColor(); } ensureSelectedTextVisible(WebCore::IntPoint(), true /* scroll if needed */); drawAnimationOverlay(regionForSelectionQuads(selection), false /* isExpandingOverlayAtConstantRate */, granularity == WordGranularity /* isStartOfSelection */); frame->selection()->setSelection(selection); if (granularity == ParagraphGranularity) findNextAnimationOverlayRegion(); return true; } void SelectionHandler::selectObject(const WebCore::IntPoint& location, TextGranularity granularity) { ASSERT(location.x() >= 0 && location.y() >= 0); ASSERT(m_webPage && m_webPage->focusedOrMainFrame() && m_webPage->focusedOrMainFrame()->selection()); Frame* focusedFrame = m_webPage->focusedOrMainFrame(); SelectionLog(Platform::LogLevelInfo, "SelectionHandler::selectObject adjusted points %s", Platform::IntPoint(location).toString().c_str()); WebCore::IntPoint relativePoint = DOMSupport::convertPointToFrame(m_webPage->mainFrame(), focusedFrame, location); VisiblePosition pointLocation(focusedFrame->visiblePositionForPoint(relativePoint)); VisibleSelection selection = VisibleSelection(pointLocation, pointLocation); // Move focus to the new node if we're not selecting in old input field. if (!m_webPage->m_inputHandler->boundingBoxForInputField().contains(relativePoint)) { Node* anchorNode = selection.start().anchorNode(); if (!anchorNode || anchorNode->isElementNode()) m_webPage->m_page->focusController()->setFocusedElement(toElement(anchorNode), focusedFrame); } m_selectionActive = expandSelectionToGranularity(focusedFrame, selection, granularity, m_webPage->m_inputHandler->isInputMode()); } void SelectionHandler::selectObject(TextGranularity granularity) { ASSERT(m_webPage && m_webPage->m_inputHandler); // Using caret location, must be inside an input field. if (!m_webPage->m_inputHandler->isInputMode()) return; ASSERT(m_webPage->focusedOrMainFrame() && m_webPage->focusedOrMainFrame()->selection()); Frame* focusedFrame = m_webPage->focusedOrMainFrame(); SelectionLog(Platform::LogLevelInfo, "SelectionHandler::selectObject using current selection"); ASSERT(focusedFrame->selection()->selectionType() != VisibleSelection::NoSelection); // Use the current selection as the selection point. VisibleSelection selectionOrigin = focusedFrame->selection()->selection(); // If this is the end of the input field, make sure we select the last word. if (m_webPage->m_inputHandler->isCaretAtEndOfText()) selectionOrigin = previousWordPosition(selectionOrigin.start()); m_selectionActive = expandSelectionToGranularity(focusedFrame, selectionOrigin, granularity, true /* isInputMode */); } void SelectionHandler::selectObject(Node* node) { if (!node) return; // Clear input focus if we're not selecting text there. if (node != m_webPage->m_inputHandler->currentFocusElement().get()) m_webPage->clearFocusNode(); m_selectionActive = true; ASSERT(m_webPage && m_webPage->focusedOrMainFrame() && m_webPage->focusedOrMainFrame()->selection()); Frame* focusedFrame = m_webPage->focusedOrMainFrame(); SelectionLog(Platform::LogLevelInfo, "SelectionHandler::selectNode"); VisibleSelection selection = VisibleSelection::selectionFromContentsOfNode(node); drawAnimationOverlay(regionForSelectionQuads(selection), false /* isExpandingOverlayAtConstantRate */, true /* isStartOfSelection */); focusedFrame->selection()->setSelection(selection); m_webPage->updateSelectionScrollView(node); } static TextDirection directionOfEnclosingBlock(FrameSelection* selection) { Node* enclosingBlockNode = enclosingBlock(selection->selection().extent().deprecatedNode()); if (!enclosingBlockNode) return LTR; if (RenderObject* renderer = enclosingBlockNode->renderer()) return renderer->style()->direction(); return LTR; } // Returns > 0 if p1 is "closer" to referencePoint, < 0 if p2 is "closer", 0 if they are equidistant. // Because text is usually arranged in horizontal rows, distance is measured along the y-axis, with x-axis used only to break ties. // If rightGravity is true, the right-most x-coordinate is chosen, otherwise teh left-most coordinate is chosen. static inline int comparePointsToReferencePoint(const WebCore::IntPoint& p1, const WebCore::IntPoint& p2, const WebCore::IntPoint& referencePoint, bool rightGravity) { int dy1 = abs(referencePoint.y() - p1.y()); int dy2 = abs(referencePoint.y() - p2.y()); if (dy1 != dy2) return dy2 - dy1; // Same y-coordinate, choose the farthest right (or left) point. if (p1.x() == p2.x()) return 0; if (p1.x() > p2.x()) return rightGravity ? 1 : -1; return rightGravity ? -1 : 1; } // NOTE/FIXME: Due to r77286, we are getting off-by-one results in the IntRect class counterpart implementation of the // methods below. As done in r89803, r77928 and a few others, lets use local method to fix it. // We should keep our eyes very open on it, since it can affect BackingStore very badly. static WebCore::IntPoint minXMinYCorner(const WebCore::IntRect& rect) { return rect.location(); } // typically topLeft static WebCore::IntPoint maxXMinYCorner(const WebCore::IntRect& rect) { return WebCore::IntPoint(rect.x() + rect.width() - 1, rect.y()); } // typically topRight static WebCore::IntPoint minXMaxYCorner(const WebCore::IntRect& rect) { return WebCore::IntPoint(rect.x(), rect.y() + rect.height() - 1); } // typically bottomLeft static WebCore::IntPoint maxXMaxYCorner(const WebCore::IntRect& rect) { return WebCore::IntPoint(rect.x() + rect.width() - 1, rect.y() + rect.height() - 1); } // typically bottomRight // The caret is a one-pixel wide line down either the right or left edge of a // rect, depending on the text direction. static inline bool caretIsOnLeft(bool isStartCaret, bool isRTL) { if (isStartCaret) return !isRTL; return isRTL; } static inline WebCore::IntPoint caretLocationForRect(const WebCore::IntRect& rect, bool isStartCaret, bool isRTL) { return caretIsOnLeft(isStartCaret, isRTL) ? minXMinYCorner(rect) : maxXMinYCorner(rect); } static inline WebCore::IntPoint caretComparisonPointForRect(const WebCore::IntRect& rect, bool isStartCaret, bool isRTL) { if (isStartCaret) return caretIsOnLeft(isStartCaret, isRTL) ? minXMinYCorner(rect) : maxXMinYCorner(rect); return caretIsOnLeft(isStartCaret, isRTL) ? minXMaxYCorner(rect) : maxXMaxYCorner(rect); } static void adjustCaretRects(WebCore::IntRect& startCaret, bool isStartCaretClippedOut, WebCore::IntRect& endCaret, bool isEndCaretClippedOut, const std::vector rectList, const WebCore::IntPoint& startReferencePoint, const WebCore::IntPoint& endReferencePoint, bool isRTL) { // startReferencePoint is the best guess at the top left of the selection; endReferencePoint is the best guess at the bottom right. if (isStartCaretClippedOut) startCaret.setLocation(DOMSupport::InvalidPoint); else { startCaret = rectList[0]; startCaret.setLocation(caretLocationForRect(startCaret, true, isRTL)); // Reset width to 1 as we are strictly interested in caret location. startCaret.setWidth(1); } if (isEndCaretClippedOut) endCaret.setLocation(DOMSupport::InvalidPoint); else { endCaret = rectList[0]; endCaret.setLocation(caretLocationForRect(endCaret, false, isRTL)); // Reset width to 1 as we are strictly interested in caret location. endCaret.setWidth(1); } if (isStartCaretClippedOut && isEndCaretClippedOut) return; for (unsigned i = 1; i < rectList.size(); i++) { WebCore::IntRect currentRect(rectList[i]); // Compare and update the start and end carets with their respective reference points. if (!isStartCaretClippedOut && comparePointsToReferencePoint( caretComparisonPointForRect(currentRect, true, isRTL), caretComparisonPointForRect(startCaret, true, isRTL), startReferencePoint, isRTL) > 0) { startCaret.setLocation(caretLocationForRect(currentRect, true, isRTL)); startCaret.setHeight(currentRect.height()); } if (!isEndCaretClippedOut && comparePointsToReferencePoint( caretComparisonPointForRect(currentRect, false, isRTL), caretComparisonPointForRect(endCaret, false, isRTL), endReferencePoint, !isRTL) > 0) { endCaret.setLocation(caretLocationForRect(currentRect, false, isRTL)); endCaret.setHeight(currentRect.height()); } } } WebCore::IntPoint SelectionHandler::clipPointToVisibleContainer(const WebCore::IntPoint& point) const { ASSERT(m_webPage->m_mainFrame && m_webPage->m_mainFrame->view()); Frame* frame = m_webPage->focusedOrMainFrame(); WebCore::IntPoint clippedPoint = DOMSupport::convertPointToFrame(m_webPage->mainFrame(), frame, point, true /* clampToTargetFrame */); if (m_webPage->m_inputHandler->isInputMode() && frame->document()->focusedElement() && frame->document()->focusedElement()->renderer()) { WebCore::IntRect boundingBox(frame->document()->focusedElement()->renderer()->absoluteBoundingBoxRect()); boundingBox.inflate(-1); clippedPoint = WebCore::IntPoint(clamp(boundingBox.x(), clippedPoint.x(), boundingBox.maxX()), clamp(boundingBox.y(), clippedPoint.y(), boundingBox.maxY())); } return clippedPoint; } static WebCore::IntPoint referencePoint(const VisiblePosition& position, const WebCore::IntRect& boundingRect, const WebCore::IntPoint& framePosition, bool isStartCaret, bool isRTL) { // If one of the carets is invalid (this happens, for instance, if the // selection ends in an empty div) fall back to using the corner of the // entire region (which is already in frame coordinates so doesn't need // adjusting). WebCore::IntRect startCaretBounds(position.absoluteCaretBounds()); startCaretBounds.move(framePosition.x(), framePosition.y()); if (startCaretBounds.isEmpty() || !boundingRect.contains(startCaretBounds)) startCaretBounds = boundingRect; return caretComparisonPointForRect(startCaretBounds, isStartCaret, isRTL); } // Check all rects in the region for a point match. The region is non-banded // and non-sorted so all must be checked. static bool regionRectListContainsPoint(const IntRectRegion& region, const WebCore::IntPoint& point) { if (!region.extents().contains(point)) return false; std::vector rectList = region.rects(); for (unsigned i = 0; i < rectList.size(); i++) { if (rectList[i].contains(point)) return true; } return false; } bool SelectionHandler::inputNodeOverridesTouch() const { if (!m_webPage->m_inputHandler->isInputMode()) return false; Node* focusedNode = m_webPage->focusedOrMainFrame()->document()->focusedElement(); if (!focusedNode || !focusedNode->isElementNode()) return false; // TODO consider caching this in InputHandler so it is only calculated once per focus. DEFINE_STATIC_LOCAL(QualifiedName, selectionTouchOverrideAttr, (nullAtom, "data-blackberry-end-selection-on-touch", nullAtom)); Element* element = toElement(focusedNode); return DOMSupport::elementAttributeState(element, selectionTouchOverrideAttr) == DOMSupport::On; } RequestedHandlePosition SelectionHandler::requestedSelectionHandlePosition(const VisibleSelection& selection) const { Element* element = DOMSupport::selectionContainerElement(selection); return DOMSupport::elementHandlePositionAttribute(element); } // Note: This is the only function in SelectionHandler in which the coordinate // system is not entirely WebKit. void SelectionHandler::selectionPositionChanged(bool forceUpdateWithoutChange) { SelectionLog(Platform::LogLevelInfo, "SelectionHandler::selectionPositionChanged forceUpdateWithoutChange = %s", forceUpdateWithoutChange ? "true" : "false"); // This method can get called during WebPage shutdown process. // If that is the case, just bail out since the client is not // in a safe state of trust to request anything else from it. if (!m_webPage->m_mainFrame) return; if (m_webPage->m_inputHandler->isInputMode() && m_webPage->m_inputHandler->processingChange()) { if (m_webPage->m_selectionOverlay) m_webPage->m_selectionOverlay->hide(); m_webPage->m_client->cancelSelectionVisuals(); // Since we're not calling notifyCaretPositionChangedIfNeeded now, we have to do so at the end of processing // to avoid dropping a notification. m_didSuppressCaretPositionChangedNotification = true; return; } notifyCaretPositionChangedIfNeeded(m_webPage->m_touchEventHandler->m_userTriggeredTouchPressOnTextInput); // Enter selection mode if selection type is RangeSelection, and disable selection if // selection is active and becomes caret selection. Frame* frame = m_webPage->focusedOrMainFrame(); if (frame->view()->needsLayout()) return; WebCore::IntPoint framePos = m_webPage->frameOffset(frame); if (m_selectionActive && (m_caretActive || frame->selection()->isNone())) m_selectionActive = false; else if (frame->selection()->isRange()) m_selectionActive = true; else if (!m_selectionActive) return; if (Node* focusedNode = frame->document()->focusedElement()) { if (focusedNode->hasTagName(HTMLNames::selectTag) || (focusedNode->isElementNode() && DOMSupport::isPopupInputField(toElement(focusedNode)))) { SelectionLog(Platform::LogLevelInfo, "SelectionHandler::selectionPositionChanged selection is on a popup control, skipping rendering."); return; } } SelectionTimingLog(Platform::LogLevelInfo, "SelectionHandler::selectionPositionChanged starting at %f", m_timer.elapsed()); WebCore::IntRect startCaret(DOMSupport::InvalidPoint, WebCore::IntSize()); WebCore::IntRect endCaret(DOMSupport::InvalidPoint, WebCore::IntSize()); // Get the text rects from the selections range. Vector quads; DOMSupport::visibleTextQuads(frame->selection()->selection(), quads); IntRectRegion unclippedRegion; regionForTextQuads(quads, unclippedRegion, false /* shouldClipToVisibleContent */); // If there is no change in selected text and the visual rects // have not changed then don't bother notifying anything. if (!forceUpdateWithoutChange && m_lastSelectionRegion.isEqual(unclippedRegion)) return; m_lastSelectionRegion = unclippedRegion; bool isRTL = directionOfEnclosingBlock(frame->selection()) == RTL; IntRectRegion visibleSelectionRegion; if (!unclippedRegion.isEmpty()) { WebCore::IntRect unclippedStartCaret; WebCore::IntRect unclippedEndCaret; WebCore::IntPoint startCaretReferencePoint = referencePoint(frame->selection()->selection().visibleStart(), unclippedRegion.extents(), framePos, true /* isStartCaret */, isRTL); WebCore::IntPoint endCaretReferencePoint = referencePoint(frame->selection()->selection().visibleEnd(), unclippedRegion.extents(), framePos, false /* isStartCaret */, isRTL); adjustCaretRects(unclippedStartCaret, false /* unclipped */, unclippedEndCaret, false /* unclipped */, unclippedRegion.rects(), startCaretReferencePoint, endCaretReferencePoint, isRTL); regionForTextQuads(quads, visibleSelectionRegion); #if SHOWDEBUG_SELECTIONHANDLER // Don't rely just on SelectionLog to avoid loop. for (unsigned i = 0; i < unclippedRegion.numRects(); i++) { SelectionLog(Platform::LogLevelInfo, "Rect list - Unmodified #%d, %s", i, unclippedRegion.rects()[i].toString().c_str()); } for (unsigned i = 0; i < visibleSelectionRegion.numRects(); i++) { SelectionLog(Platform::LogLevelInfo, "Rect list - Clipped to Visible #%d, %s", i, visibleSelectionRegion.rects()[i].toString().c_str()); } #endif bool shouldCareAboutPossibleClippedOutSelection = frame != m_webPage->mainFrame() || m_webPage->m_inputHandler->isInputMode(); if (!visibleSelectionRegion.isEmpty() || shouldCareAboutPossibleClippedOutSelection) { // Adjust the handle markers to be at the end of the painted rect. When selecting links // and other elements that may have a larger visible area than needs to be rendered a gap // can exist between the handle and overlay region. bool shouldClipStartCaret = !regionRectListContainsPoint(visibleSelectionRegion, unclippedStartCaret.location()); bool shouldClipEndCaret = !regionRectListContainsPoint(visibleSelectionRegion, unclippedEndCaret.location()); // Find the top corner and bottom corner. adjustCaretRects(startCaret, shouldClipStartCaret, endCaret, shouldClipEndCaret, visibleSelectionRegion.rects(), startCaretReferencePoint, endCaretReferencePoint, isRTL); } } SelectionLog(Platform::LogLevelInfo, "SelectionHandler::selectionPositionChanged Start Rect=%s End Rect=%s", Platform::IntRect(startCaret).toString().c_str(), Platform::IntRect(endCaret).toString().c_str()); if (m_webPage->m_selectionOverlay) m_webPage->m_selectionOverlay->draw(visibleSelectionRegion); VisibleSelection currentSelection = frame->selection()->selection(); SelectionDetails details(startCaret, endCaret, visibleSelectionRegion, inputNodeOverridesTouch(), m_lastSelection != currentSelection, requestedSelectionHandlePosition(frame->selection()->selection()), isRTL); m_webPage->m_client->notifySelectionDetailsChanged(details); m_lastSelection = currentSelection; SelectionTimingLog(Platform::LogLevelInfo, "SelectionHandler::selectionPositionChanged completed at %f", m_timer.elapsed()); } void SelectionHandler::notifyCaretPositionChangedIfNeeded(bool userTouchTriggeredOnTextField) { m_didSuppressCaretPositionChangedNotification = false; if (m_caretActive || (m_webPage->m_inputHandler->isInputMode() && m_webPage->focusedOrMainFrame()->selection()->isCaret())) { // This may update the caret to no longer be active. caretPositionChanged(userTouchTriggeredOnTextField); } } void SelectionHandler::caretPositionChanged(bool userTouchTriggeredOnTextField) { SelectionLog(Platform::LogLevelInfo, "SelectionHandler::caretPositionChanged"); WebCore::IntRect caretLocation; // If the input field is not active, we must be turning off the caret. if (!m_webPage->m_inputHandler->isInputMode() && m_caretActive) { m_caretActive = false; // Send an empty caret change to turn off the caret. m_webPage->m_client->notifyCaretChanged(caretLocation, userTouchTriggeredOnTextField); return; } ASSERT(m_webPage && m_webPage->focusedOrMainFrame() && m_webPage->focusedOrMainFrame()->selection()); // This function should only reach this point if input mode is active. ASSERT(m_webPage->m_inputHandler->isInputMode()); WebCore::IntRect clippingRectForContent(clippingRectForVisibleContent()); WebCore::IntPoint frameOffset(m_webPage->frameOffset(m_webPage->focusedOrMainFrame())); if (m_webPage->focusedOrMainFrame()->selection()->selectionType() == VisibleSelection::CaretSelection) { caretLocation = startCaretViewportRect(frameOffset); if (!caretLocation.isEmpty()) caretLocation.intersect(clippingRectForContent); // Clip against the containing frame and node boundaries. } m_caretActive = !caretLocation.isEmpty(); SelectionLog(Platform::LogLevelInfo, "SelectionHandler::caretPositionChanged caret Rect %s", Platform::IntRect(caretLocation).toString().c_str()); bool isSingleLineInput = m_caretActive && !m_webPage->m_inputHandler->isMultilineInputMode(); WebCore::IntRect nodeBoundingBox = isSingleLineInput ? m_webPage->m_inputHandler->boundingBoxForInputField() : WebCore::IntRect(); if (!nodeBoundingBox.isEmpty()) { nodeBoundingBox.moveBy(frameOffset); // Clip against the containing frame and node boundaries. nodeBoundingBox.intersect(clippingRectForContent); } SelectionLog(Platform::LogLevelInfo, "SelectionHandler::caretPositionChanged: %s line input, single line bounding box %s%s", isSingleLineInput ? "single" : "multi", Platform::IntRect(nodeBoundingBox).toString().c_str(), m_webPage->m_inputHandler->elementText().isEmpty() ? ", empty text field" : ""); m_webPage->m_client->notifyCaretChanged(caretLocation, userTouchTriggeredOnTextField, isSingleLineInput, nodeBoundingBox, m_webPage->m_inputHandler->elementText().isEmpty()); } bool SelectionHandler::selectionContains(const WebCore::IntPoint& point) { ASSERT(m_webPage && m_webPage->focusedOrMainFrame() && m_webPage->focusedOrMainFrame()->selection()); return m_webPage->focusedOrMainFrame()->selection()->contains(point); } } }