diff options
Diffstat (limited to 'Source/WebCore/editing/SelectionController.cpp')
-rw-r--r-- | Source/WebCore/editing/SelectionController.cpp | 1802 |
1 files changed, 1802 insertions, 0 deletions
diff --git a/Source/WebCore/editing/SelectionController.cpp b/Source/WebCore/editing/SelectionController.cpp new file mode 100644 index 0000000..eca0711 --- /dev/null +++ b/Source/WebCore/editing/SelectionController.cpp @@ -0,0 +1,1802 @@ +/* + * Copyright (C) 2004, 2008, 2009, 2010 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "SelectionController.h" + +#include "CharacterData.h" +#include "DeleteSelectionCommand.h" +#include "Document.h" +#include "Editor.h" +#include "EditorClient.h" +#include "Element.h" +#include "EventHandler.h" +#include "ExceptionCode.h" +#include "FloatQuad.h" +#include "FocusController.h" +#include "Frame.h" +#include "FrameTree.h" +#include "FrameView.h" +#include "GraphicsContext.h" +#include "HTMLFormElement.h" +#include "HTMLFrameElementBase.h" +#include "HTMLInputElement.h" +#include "HTMLNames.h" +#include "HitTestRequest.h" +#include "HitTestResult.h" +#include "Page.h" +#include "Range.h" +#include "RenderLayer.h" +#include "RenderTextControl.h" +#include "RenderTheme.h" +#include "RenderView.h" +#include "RenderWidget.h" +#include "SecureTextInput.h" +#include "Settings.h" +#include "TextIterator.h" +#include "TypingCommand.h" +#include "htmlediting.h" +#include "visible_units.h" +#ifdef ANDROID_ALLOW_TURNING_OFF_CARET +#include "WebViewCore.h" +#endif +#include <stdio.h> +#include <wtf/text/CString.h> + +#define EDIT_DEBUG 0 + +namespace WebCore { + +using namespace HTMLNames; + +const int NoXPosForVerticalArrowNavigation = INT_MIN; + +SelectionController::SelectionController(Frame* frame, bool isDragCaretController) + : m_frame(frame) + , m_xPosForVerticalArrowNavigation(NoXPosForVerticalArrowNavigation) + , m_granularity(CharacterGranularity) + , m_caretBlinkTimer(this, &SelectionController::caretBlinkTimerFired) + , m_caretRectNeedsUpdate(true) + , m_absCaretBoundsDirty(true) + , m_isDragCaretController(isDragCaretController) + , m_isCaretBlinkingSuspended(false) + , m_focused(frame && frame->page() && frame->page()->focusController()->focusedFrame() == frame) + , m_caretVisible(isDragCaretController) + , m_caretPaint(true) +{ + setIsDirectional(false); +} + +void SelectionController::moveTo(const VisiblePosition &pos, bool userTriggered, CursorAlignOnScroll align) +{ + setSelection(VisibleSelection(pos.deepEquivalent(), pos.deepEquivalent(), pos.affinity()), true, true, userTriggered, align); +} + +void SelectionController::moveTo(const VisiblePosition &base, const VisiblePosition &extent, bool userTriggered) +{ + setSelection(VisibleSelection(base.deepEquivalent(), extent.deepEquivalent(), base.affinity()), true, true, userTriggered); +} + +void SelectionController::moveTo(const Position &pos, EAffinity affinity, bool userTriggered) +{ + setSelection(VisibleSelection(pos, affinity), true, true, userTriggered); +} + +void SelectionController::moveTo(const Range *r, EAffinity affinity, bool userTriggered) +{ + VisibleSelection selection = r ? VisibleSelection(r->startPosition(), r->endPosition(), affinity) : VisibleSelection(Position(), Position(), affinity); + setSelection(selection, true, true, userTriggered); +} + +void SelectionController::moveTo(const Position &base, const Position &extent, EAffinity affinity, bool userTriggered) +{ + setSelection(VisibleSelection(base, extent, affinity), true, true, userTriggered); +} + +void SelectionController::setSelection(const VisibleSelection& s, bool closeTyping, bool shouldClearTypingStyle, bool userTriggered, CursorAlignOnScroll align, TextGranularity granularity, DirectionalityPolicy directionalityPolicy) +{ + m_granularity = granularity; + + setIsDirectional(directionalityPolicy == MakeDirectionalSelection); + + if (m_isDragCaretController) { + invalidateCaretRect(); + m_selection = s; + m_caretRectNeedsUpdate = true; + invalidateCaretRect(); + updateCaretRect(); + return; + } + if (!m_frame) { + m_selection = s; + return; + } + + Node* baseNode = s.base().node(); + Document* document = 0; + if (baseNode) + document = baseNode->document(); + + // <http://bugs.webkit.org/show_bug.cgi?id=23464>: Infinite recursion at SelectionController::setSelection + // if document->frame() == m_frame we can get into an infinite loop + if (document && document->frame() && document->frame() != m_frame && document != m_frame->document()) { + document->frame()->selection()->setSelection(s, closeTyping, shouldClearTypingStyle, userTriggered); + return; + } + + if (closeTyping) + TypingCommand::closeTyping(m_frame->editor()->lastEditCommand()); + + if (shouldClearTypingStyle) + clearTypingStyle(); + + if (m_selection == s) + return; + + VisibleSelection oldSelection = m_selection; + + m_selection = s; + + m_caretRectNeedsUpdate = true; + + if (!s.isNone()) + setFocusedNodeIfNeeded(); + + updateAppearance(); + + // Always clear the x position used for vertical arrow navigation. + // It will be restored by the vertical arrow navigation code if necessary. + m_xPosForVerticalArrowNavigation = NoXPosForVerticalArrowNavigation; + selectFrameElementInParentIfFullySelected(); + notifyRendererOfSelectionChange(userTriggered); + m_frame->editor()->respondToChangedSelection(oldSelection, closeTyping); + if (userTriggered) { + ScrollAlignment alignment; + + if (m_frame->editor()->behavior().shouldCenterAlignWhenSelectionIsRevealed()) + alignment = (align == AlignCursorOnScrollAlways) ? ScrollAlignment::alignCenterAlways : ScrollAlignment::alignCenterIfNeeded; + else + alignment = (align == AlignCursorOnScrollAlways) ? ScrollAlignment::alignTopAlways : ScrollAlignment::alignToEdgeIfNeeded; + + revealSelection(alignment, true); + } + + notifyAccessibilityForSelectionChange(); +} + +static bool removingNodeRemovesPosition(Node* node, const Position& position) +{ + if (!position.node()) + return false; + + if (position.node() == node) + return true; + + if (!node->isElementNode()) + return false; + + Element* element = static_cast<Element*>(node); + return element->contains(position.node()) || element->contains(position.node()->shadowAncestorNode()); +} + +void SelectionController::nodeWillBeRemoved(Node *node) +{ + if (isNone()) + return; + + // There can't be a selection inside a fragment, so if a fragment's node is being removed, + // the selection in the document that created the fragment needs no adjustment. + if (node && highestAncestor(node)->nodeType() == Node::DOCUMENT_FRAGMENT_NODE) + return; + + respondToNodeModification(node, removingNodeRemovesPosition(node, m_selection.base()), removingNodeRemovesPosition(node, m_selection.extent()), + removingNodeRemovesPosition(node, m_selection.start()), removingNodeRemovesPosition(node, m_selection.end())); +} + +void SelectionController::respondToNodeModification(Node* node, bool baseRemoved, bool extentRemoved, bool startRemoved, bool endRemoved) +{ + bool clearRenderTreeSelection = false; + bool clearDOMTreeSelection = false; + + if (startRemoved || endRemoved) { + // FIXME: When endpoints are removed, we should just alter the selection, instead of blowing it away. + clearRenderTreeSelection = true; + clearDOMTreeSelection = true; + } else if (baseRemoved || extentRemoved) { + // The base and/or extent are about to be removed, but the start and end aren't. + // Change the base and extent to the start and end, but don't re-validate the + // selection, since doing so could move the start and end into the node + // that is about to be removed. + if (m_selection.isBaseFirst()) + m_selection.setWithoutValidation(m_selection.start(), m_selection.end()); + else + m_selection.setWithoutValidation(m_selection.end(), m_selection.start()); + } else if (m_selection.firstRange()) { + ExceptionCode ec = 0; + Range::CompareResults compareResult = m_selection.firstRange()->compareNode(node, ec); + if (!ec && (compareResult == Range::NODE_BEFORE_AND_AFTER || compareResult == Range::NODE_INSIDE)) { + // If we did nothing here, when this node's renderer was destroyed, the rect that it + // occupied would be invalidated, but, selection gaps that change as a result of + // the removal wouldn't be invalidated. + // FIXME: Don't do so much unnecessary invalidation. + clearRenderTreeSelection = true; + } + } + + if (clearRenderTreeSelection) { + RefPtr<Document> document = m_selection.start().node()->document(); + document->updateStyleIfNeeded(); + if (RenderView* view = toRenderView(document->renderer())) + view->clearSelection(); + } + + if (clearDOMTreeSelection) + setSelection(VisibleSelection(), false, false); +} + +enum EndPointType { EndPointIsStart, EndPointIsEnd }; + +static bool shouldRemovePositionAfterAdoptingTextReplacement(Position& position, EndPointType type, CharacterData* node, unsigned offset, unsigned oldLength, unsigned newLength) +{ + if (!position.anchorNode() || position.anchorNode() != node || position.anchorType() != Position::PositionIsOffsetInAnchor) + return false; + + if (static_cast<unsigned>(position.offsetInContainerNode()) > offset && static_cast<unsigned>(position.offsetInContainerNode()) < offset + oldLength) + return true; + + if ((type == EndPointIsStart && static_cast<unsigned>(position.offsetInContainerNode()) >= offset + oldLength) + || (type == EndPointIsEnd && static_cast<unsigned>(position.offsetInContainerNode()) > offset + oldLength)) + position.moveToOffset(position.offsetInContainerNode() - oldLength + newLength); + + return false; +} + +void SelectionController::textWillBeReplaced(CharacterData* node, unsigned offset, unsigned oldLength, unsigned newLength) +{ + // The fragment check is a performance optimization. See http://trac.webkit.org/changeset/30062. + if (isNone() || !node || highestAncestor(node)->nodeType() == Node::DOCUMENT_FRAGMENT_NODE) + return; + + Position base = m_selection.base(); + Position extent = m_selection.extent(); + Position start = m_selection.start(); + Position end = m_selection.end(); + bool shouldRemoveBase = shouldRemovePositionAfterAdoptingTextReplacement(base, m_selection.isBaseFirst() ? EndPointIsStart : EndPointIsEnd, node, offset, oldLength, newLength); + bool shouldRemoveExtent = shouldRemovePositionAfterAdoptingTextReplacement(extent, m_selection.isBaseFirst() ? EndPointIsEnd : EndPointIsStart, node, offset, oldLength, newLength); + bool shouldRemoveStart = shouldRemovePositionAfterAdoptingTextReplacement(start, EndPointIsStart, node, offset, oldLength, newLength); + bool shouldRemoveEnd = shouldRemovePositionAfterAdoptingTextReplacement(end, EndPointIsEnd, node, offset, oldLength, newLength); + + if ((base != m_selection.base() || extent != m_selection.extent() || start != m_selection.start() || end != m_selection.end()) + && !shouldRemoveStart && !shouldRemoveEnd) { + if (!shouldRemoveBase && !shouldRemoveExtent) + m_selection.setWithoutValidation(base, extent); + else { + if (m_selection.isBaseFirst()) + m_selection.setWithoutValidation(m_selection.start(), m_selection.end()); + else + m_selection.setWithoutValidation(m_selection.end(), m_selection.start()); + } + } + + respondToNodeModification(node, shouldRemoveBase, shouldRemoveExtent, shouldRemoveStart, shouldRemoveEnd); +} + +void SelectionController::setIsDirectional(bool isDirectional) +{ + m_isDirectional = !m_frame || m_frame->editor()->behavior().shouldConsiderSelectionAsDirectional() || isDirectional; +} + +void SelectionController::willBeModified(EAlteration alter, SelectionDirection direction) +{ + if (alter != AlterationExtend) + return; + + Position start = m_selection.start(); + Position end = m_selection.end(); + + if (m_isDirectional) { + // Make base and extent match start and end so we extend the user-visible selection. + // This only matters for cases where base and extend point to different positions than + // start and end (e.g. after a double-click to select a word). + if (m_selection.isBaseFirst()) { + m_selection.setBase(start); + m_selection.setExtent(end); + } else { + m_selection.setBase(end); + m_selection.setExtent(start); + } + } else { + // FIXME: This is probably not correct for right and left when the direction is RTL. + switch (direction) { + case DirectionRight: + case DirectionForward: + m_selection.setBase(start); + m_selection.setExtent(end); + break; + case DirectionLeft: + case DirectionBackward: + m_selection.setBase(end); + m_selection.setExtent(start); + break; + } + } +} + +TextDirection SelectionController::directionOfEnclosingBlock() +{ + Node* enclosingBlockNode = enclosingBlock(m_selection.extent().node()); + if (!enclosingBlockNode) + return LTR; + RenderObject* renderer = enclosingBlockNode->renderer(); + if (renderer) + return renderer->style()->direction(); + return LTR; +} + +VisiblePosition SelectionController::positionForPlatform(bool isGetStart) const +{ + Settings* settings = m_frame ? m_frame->settings() : 0; + if (settings && settings->editingBehaviorType() == EditingMacBehavior) + return isGetStart ? m_selection.visibleStart() : m_selection.visibleEnd(); + // Linux and Windows always extend selections from the extent endpoint. + // FIXME: VisibleSelection should be fixed to ensure as an invariant that + // base/extent always point to the same nodes as start/end, but which points + // to which depends on the value of isBaseFirst. Then this can be changed + // to just return m_sel.extent(). + return m_selection.isBaseFirst() ? m_selection.visibleEnd() : m_selection.visibleStart(); +} + +VisiblePosition SelectionController::startForPlatform() const +{ + return positionForPlatform(true); +} + +VisiblePosition SelectionController::endForPlatform() const +{ + return positionForPlatform(false); +} + +VisiblePosition SelectionController::modifyExtendingRight(TextGranularity granularity) +{ + VisiblePosition pos(m_selection.extent(), m_selection.affinity()); + + // The difference between modifyExtendingRight and modifyExtendingForward is: + // modifyExtendingForward always extends forward logically. + // modifyExtendingRight behaves the same as modifyExtendingForward except for extending character or word, + // it extends forward logically if the enclosing block is LTR direction, + // but it extends backward logically if the enclosing block is RTL direction. + switch (granularity) { + case CharacterGranularity: + if (directionOfEnclosingBlock() == LTR) + pos = pos.next(true); + else + pos = pos.previous(true); + break; + case WordGranularity: + if (directionOfEnclosingBlock() == LTR) + pos = nextWordPosition(pos); + else + pos = previousWordPosition(pos); + break; + case SentenceGranularity: + case LineGranularity: + case ParagraphGranularity: + case SentenceBoundary: + case LineBoundary: + case ParagraphBoundary: + case DocumentBoundary: + // FIXME: implement all of the above? + pos = modifyExtendingForward(granularity); + } + return pos; +} + +VisiblePosition SelectionController::modifyExtendingForward(TextGranularity granularity) +{ + VisiblePosition pos(m_selection.extent(), m_selection.affinity()); + switch (granularity) { + case CharacterGranularity: + pos = pos.next(true); + break; + case WordGranularity: + pos = nextWordPosition(pos); + break; + case SentenceGranularity: + pos = nextSentencePosition(pos); + break; + case LineGranularity: + pos = nextLinePosition(pos, xPosForVerticalArrowNavigation(EXTENT)); + break; + case ParagraphGranularity: + pos = nextParagraphPosition(pos, xPosForVerticalArrowNavigation(EXTENT)); + break; + case SentenceBoundary: + pos = endOfSentence(endForPlatform()); + break; + case LineBoundary: + pos = logicalEndOfLine(endForPlatform()); + break; + case ParagraphBoundary: + pos = endOfParagraph(endForPlatform()); + break; + case DocumentBoundary: + pos = endForPlatform(); + if (isEditablePosition(pos.deepEquivalent())) + pos = endOfEditableContent(pos); + else + pos = endOfDocument(pos); + break; + } + + return pos; +} + +VisiblePosition SelectionController::modifyMovingRight(TextGranularity granularity) +{ + VisiblePosition pos; + switch (granularity) { + case CharacterGranularity: + if (isRange()) + pos = VisiblePosition(m_selection.end(), m_selection.affinity()); + else + pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).right(true); + break; + case WordGranularity: + case SentenceGranularity: + case LineGranularity: + case ParagraphGranularity: + case SentenceBoundary: + case ParagraphBoundary: + case DocumentBoundary: + // FIXME: Implement all of the above. + pos = modifyMovingForward(granularity); + break; + case LineBoundary: + pos = rightBoundaryOfLine(startForPlatform(), directionOfEnclosingBlock()); + break; + } + return pos; +} + +VisiblePosition SelectionController::modifyMovingForward(TextGranularity granularity) +{ + VisiblePosition pos; + // FIXME: Stay in editable content for the less common granularities. + switch (granularity) { + case CharacterGranularity: + if (isRange()) + pos = VisiblePosition(m_selection.end(), m_selection.affinity()); + else + pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).next(true); + break; + case WordGranularity: + pos = nextWordPosition(VisiblePosition(m_selection.extent(), m_selection.affinity())); + break; + case SentenceGranularity: + pos = nextSentencePosition(VisiblePosition(m_selection.extent(), m_selection.affinity())); + break; + case LineGranularity: { + // down-arrowing from a range selection that ends at the start of a line needs + // to leave the selection at that line start (no need to call nextLinePosition!) + pos = endForPlatform(); + if (!isRange() || !isStartOfLine(pos)) + pos = nextLinePosition(pos, xPosForVerticalArrowNavigation(START)); + break; + } + case ParagraphGranularity: + pos = nextParagraphPosition(endForPlatform(), xPosForVerticalArrowNavigation(START)); + break; + case SentenceBoundary: + pos = endOfSentence(endForPlatform()); + break; + case LineBoundary: + pos = logicalEndOfLine(endForPlatform()); + break; + case ParagraphBoundary: + pos = endOfParagraph(endForPlatform()); + break; + case DocumentBoundary: + pos = endForPlatform(); + if (isEditablePosition(pos.deepEquivalent())) + pos = endOfEditableContent(pos); + else + pos = endOfDocument(pos); + break; + } + return pos; +} + +VisiblePosition SelectionController::modifyExtendingLeft(TextGranularity granularity) +{ + VisiblePosition pos(m_selection.extent(), m_selection.affinity()); + + // The difference between modifyExtendingLeft and modifyExtendingBackward is: + // modifyExtendingBackward always extends backward logically. + // modifyExtendingLeft behaves the same as modifyExtendingBackward except for extending character or word, + // it extends backward logically if the enclosing block is LTR direction, + // but it extends forward logically if the enclosing block is RTL direction. + switch (granularity) { + case CharacterGranularity: + if (directionOfEnclosingBlock() == LTR) + pos = pos.previous(true); + else + pos = pos.next(true); + break; + case WordGranularity: + if (directionOfEnclosingBlock() == LTR) + pos = previousWordPosition(pos); + else + pos = nextWordPosition(pos); + break; + case SentenceGranularity: + case LineGranularity: + case ParagraphGranularity: + case SentenceBoundary: + case LineBoundary: + case ParagraphBoundary: + case DocumentBoundary: + pos = modifyExtendingBackward(granularity); + } + return pos; +} + +VisiblePosition SelectionController::modifyExtendingBackward(TextGranularity granularity) +{ + VisiblePosition pos(m_selection.extent(), m_selection.affinity()); + + // Extending a selection backward by word or character from just after a table selects + // the table. This "makes sense" from the user perspective, esp. when deleting. + // It was done here instead of in VisiblePosition because we want VPs to iterate + // over everything. + switch (granularity) { + case CharacterGranularity: + pos = pos.previous(true); + break; + case WordGranularity: + pos = previousWordPosition(pos); + break; + case SentenceGranularity: + pos = previousSentencePosition(pos); + break; + case LineGranularity: + pos = previousLinePosition(pos, xPosForVerticalArrowNavigation(EXTENT)); + break; + case ParagraphGranularity: + pos = previousParagraphPosition(pos, xPosForVerticalArrowNavigation(EXTENT)); + break; + case SentenceBoundary: + pos = startOfSentence(startForPlatform()); + break; + case LineBoundary: + pos = logicalStartOfLine(startForPlatform()); + break; + case ParagraphBoundary: + pos = startOfParagraph(startForPlatform()); + break; + case DocumentBoundary: + pos = startForPlatform(); + if (isEditablePosition(pos.deepEquivalent())) + pos = startOfEditableContent(pos); + else + pos = startOfDocument(pos); + break; + } + return pos; +} + +VisiblePosition SelectionController::modifyMovingLeft(TextGranularity granularity) +{ + VisiblePosition pos; + switch (granularity) { + case CharacterGranularity: + if (isRange()) + pos = VisiblePosition(m_selection.start(), m_selection.affinity()); + else + pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).left(true); + break; + case WordGranularity: + case SentenceGranularity: + case LineGranularity: + case ParagraphGranularity: + case SentenceBoundary: + case ParagraphBoundary: + case DocumentBoundary: + // FIXME: Implement all of the above. + pos = modifyMovingBackward(granularity); + break; + case LineBoundary: + pos = leftBoundaryOfLine(startForPlatform(), directionOfEnclosingBlock()); + break; + } + return pos; +} + +VisiblePosition SelectionController::modifyMovingBackward(TextGranularity granularity) +{ + VisiblePosition pos; + switch (granularity) { + case CharacterGranularity: + if (isRange()) + pos = VisiblePosition(m_selection.start(), m_selection.affinity()); + else + pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).previous(true); + break; + case WordGranularity: + pos = previousWordPosition(VisiblePosition(m_selection.extent(), m_selection.affinity())); + break; + case SentenceGranularity: + pos = previousSentencePosition(VisiblePosition(m_selection.extent(), m_selection.affinity())); + break; + case LineGranularity: + pos = previousLinePosition(startForPlatform(), xPosForVerticalArrowNavigation(START)); + break; + case ParagraphGranularity: + pos = previousParagraphPosition(startForPlatform(), xPosForVerticalArrowNavigation(START)); + break; + case SentenceBoundary: + pos = startOfSentence(startForPlatform()); + break; + case LineBoundary: + pos = logicalStartOfLine(startForPlatform()); + break; + case ParagraphBoundary: + pos = startOfParagraph(startForPlatform()); + break; + case DocumentBoundary: + pos = startForPlatform(); + if (isEditablePosition(pos.deepEquivalent())) + pos = startOfEditableContent(pos); + else + pos = startOfDocument(pos); + break; + } + return pos; +} + +static bool isBoundary(TextGranularity granularity) +{ + return granularity == LineBoundary || granularity == ParagraphBoundary || granularity == DocumentBoundary; +} + +bool SelectionController::modify(EAlteration alter, SelectionDirection direction, TextGranularity granularity, bool userTriggered) +{ + if (userTriggered) { + SelectionController trialSelectionController; + trialSelectionController.setSelection(m_selection); + trialSelectionController.setIsDirectional(m_isDirectional); + trialSelectionController.modify(alter, direction, granularity, false); + + bool change = shouldChangeSelection(trialSelectionController.selection()); + if (!change) + return false; + } + + willBeModified(alter, direction); + + bool wasRange = m_selection.isRange(); + Position originalStartPosition = m_selection.start(); + VisiblePosition position; + switch (direction) { + case DirectionRight: + if (alter == AlterationMove) + position = modifyMovingRight(granularity); + else + position = modifyExtendingRight(granularity); + break; + case DirectionForward: + if (alter == AlterationExtend) + position = modifyExtendingForward(granularity); + else + position = modifyMovingForward(granularity); + break; + case DirectionLeft: + if (alter == AlterationMove) + position = modifyMovingLeft(granularity); + else + position = modifyExtendingLeft(granularity); + break; + case DirectionBackward: + if (alter == AlterationExtend) + position = modifyExtendingBackward(granularity); + else + position = modifyMovingBackward(granularity); + break; + } + + if (position.isNull()) + return false; + + if (isSpatialNavigationEnabled(m_frame)) + if (!wasRange && alter == AlterationMove && position == originalStartPosition) + return false; + + // Some of the above operations set an xPosForVerticalArrowNavigation. + // Setting a selection will clear it, so save it to possibly restore later. + // Note: the START position type is arbitrary because it is unused, it would be + // the requested position type if there were no xPosForVerticalArrowNavigation set. + int x = xPosForVerticalArrowNavigation(START); + + switch (alter) { + case AlterationMove: + moveTo(position, userTriggered); + break; + case AlterationExtend: + // Standard Mac behavior when extending to a boundary is grow the selection rather than leaving the + // base in place and moving the extent. Matches NSTextView. + if (!m_frame || !m_frame->editor()->behavior().shouldAlwaysGrowSelectionWhenExtendingToBoundary() || m_selection.isCaret() || !isBoundary(granularity)) + setExtent(position, userTriggered); + else { + if (direction == DirectionForward || direction == DirectionRight) + setEnd(position, userTriggered); + else + setStart(position, userTriggered); + } + break; + } + + if (granularity == LineGranularity || granularity == ParagraphGranularity) + m_xPosForVerticalArrowNavigation = x; + + if (userTriggered) + m_granularity = CharacterGranularity; + + + setCaretRectNeedsUpdate(); + + setIsDirectional(alter == AlterationExtend); + + return true; +} + +// FIXME: Maybe baseline would be better? +static bool absoluteCaretY(const VisiblePosition &c, int &y) +{ + IntRect rect = c.absoluteCaretBounds(); + if (rect.isEmpty()) + return false; + y = rect.y() + rect.height() / 2; + return true; +} + +bool SelectionController::modify(EAlteration alter, int verticalDistance, bool userTriggered, CursorAlignOnScroll align) +{ + if (!verticalDistance) + return false; + + if (userTriggered) { + SelectionController trialSelectionController; + trialSelectionController.setSelection(m_selection); + trialSelectionController.setIsDirectional(m_isDirectional); + trialSelectionController.modify(alter, verticalDistance, false); + + bool change = shouldChangeSelection(trialSelectionController.selection()); + if (!change) + return false; + } + + bool up = verticalDistance < 0; + if (up) + verticalDistance = -verticalDistance; + + willBeModified(alter, up ? DirectionBackward : DirectionForward); + + VisiblePosition pos; + int xPos = 0; + switch (alter) { + case AlterationMove: + pos = VisiblePosition(up ? m_selection.start() : m_selection.end(), m_selection.affinity()); + xPos = xPosForVerticalArrowNavigation(up ? START : END); + m_selection.setAffinity(up ? UPSTREAM : DOWNSTREAM); + break; + case AlterationExtend: + pos = VisiblePosition(m_selection.extent(), m_selection.affinity()); + xPos = xPosForVerticalArrowNavigation(EXTENT); + m_selection.setAffinity(DOWNSTREAM); + break; + } + + int startY; + if (!absoluteCaretY(pos, startY)) + return false; + if (up) + startY = -startY; + int lastY = startY; + + VisiblePosition result; + VisiblePosition next; + for (VisiblePosition p = pos; ; p = next) { + next = (up ? previousLinePosition : nextLinePosition)(p, xPos); + if (next.isNull() || next == p) + break; + int nextY; + if (!absoluteCaretY(next, nextY)) + break; + if (up) + nextY = -nextY; + if (nextY - startY > verticalDistance) + break; + if (nextY >= lastY) { + lastY = nextY; + result = next; + } + } + + if (result.isNull()) + return false; + + switch (alter) { + case AlterationMove: + moveTo(result, userTriggered, align); + break; + case AlterationExtend: + setExtent(result, userTriggered); + break; + } + + if (userTriggered) + m_granularity = CharacterGranularity; + + setIsDirectional(alter == AlterationExtend); + + return true; +} + +int SelectionController::xPosForVerticalArrowNavigation(EPositionType type) +{ + int x = 0; + + if (isNone()) + return x; + + Position pos; + switch (type) { + case START: + pos = m_selection.start(); + break; + case END: + pos = m_selection.end(); + break; + case BASE: + pos = m_selection.base(); + break; + case EXTENT: + pos = m_selection.extent(); + break; + } + + Frame* frame = pos.node()->document()->frame(); + if (!frame) + return x; + + if (m_xPosForVerticalArrowNavigation == NoXPosForVerticalArrowNavigation) { + VisiblePosition visiblePosition(pos, m_selection.affinity()); + // VisiblePosition creation can fail here if a node containing the selection becomes visibility:hidden + // after the selection is created and before this function is called. + x = visiblePosition.isNotNull() ? visiblePosition.xOffsetForVerticalNavigation() : 0; + m_xPosForVerticalArrowNavigation = x; + } else + x = m_xPosForVerticalArrowNavigation; + + return x; +} + +void SelectionController::clear() +{ + m_granularity = CharacterGranularity; + setSelection(VisibleSelection()); +} + +void SelectionController::setStart(const VisiblePosition &pos, bool userTriggered) +{ + if (m_selection.isBaseFirst()) + setBase(pos, userTriggered); + else + setExtent(pos, userTriggered); +} + +void SelectionController::setEnd(const VisiblePosition &pos, bool userTriggered) +{ + if (m_selection.isBaseFirst()) + setExtent(pos, userTriggered); + else + setBase(pos, userTriggered); +} + +void SelectionController::setBase(const VisiblePosition &pos, bool userTriggered) +{ + setSelection(VisibleSelection(pos.deepEquivalent(), m_selection.extent(), pos.affinity()), true, true, userTriggered); +} + +void SelectionController::setExtent(const VisiblePosition &pos, bool userTriggered) +{ + setSelection(VisibleSelection(m_selection.base(), pos.deepEquivalent(), pos.affinity()), true, true, userTriggered); +} + +void SelectionController::setBase(const Position &pos, EAffinity affinity, bool userTriggered) +{ + setSelection(VisibleSelection(pos, m_selection.extent(), affinity), true, true, userTriggered); +} + +void SelectionController::setExtent(const Position &pos, EAffinity affinity, bool userTriggered) +{ + setSelection(VisibleSelection(m_selection.base(), pos, affinity), true, true, userTriggered); +} + +void SelectionController::setCaretRectNeedsUpdate(bool flag) +{ + m_caretRectNeedsUpdate = flag; +} + +void SelectionController::updateCaretRect() +{ + if (isNone() || !m_selection.start().node()->inDocument() || !m_selection.end().node()->inDocument()) { + m_caretRect = IntRect(); + return; + } + + m_selection.start().node()->document()->updateStyleIfNeeded(); + + m_caretRect = IntRect(); + + if (isCaret()) { + VisiblePosition pos(m_selection.start(), m_selection.affinity()); + if (pos.isNotNull()) { + ASSERT(pos.deepEquivalent().node()->renderer()); + + // First compute a rect local to the renderer at the selection start + RenderObject* renderer; + IntRect localRect = pos.localCaretRect(renderer); + + // Get the renderer that will be responsible for painting the caret (which + // is either the renderer we just found, or one of its containers) + RenderObject* caretPainter = caretRenderer(); + + // Compute an offset between the renderer and the caretPainter + bool unrooted = false; + while (renderer != caretPainter) { + RenderObject* containerObject = renderer->container(); + if (!containerObject) { + unrooted = true; + break; + } + localRect.move(renderer->offsetFromContainer(containerObject, localRect.location())); + renderer = containerObject; + } + + if (!unrooted) + m_caretRect = localRect; + + m_absCaretBoundsDirty = true; + } + } + + m_caretRectNeedsUpdate = false; +} + +RenderObject* SelectionController::caretRenderer() const +{ + Node* node = m_selection.start().node(); + if (!node) + return 0; + + RenderObject* renderer = node->renderer(); + if (!renderer) + return 0; + + // if caretNode is a block and caret is inside it then caret should be painted by that block + bool paintedByBlock = renderer->isBlockFlow() && caretRendersInsideNode(node); + return paintedByBlock ? renderer : renderer->containingBlock(); +} + +IntRect SelectionController::localCaretRect() +{ + if (m_caretRectNeedsUpdate) + updateCaretRect(); + + return m_caretRect; +} + +IntRect SelectionController::absoluteBoundsForLocalRect(const IntRect& rect) const +{ + RenderObject* caretPainter = caretRenderer(); + if (!caretPainter) + return IntRect(); + + IntRect localRect(rect); + if (caretPainter->isBox()) + toRenderBox(caretPainter)->flipForWritingMode(localRect); + return caretPainter->localToAbsoluteQuad(FloatRect(localRect)).enclosingBoundingBox(); +} + +IntRect SelectionController::absoluteCaretBounds() +{ + recomputeCaretRect(); + return m_absCaretBounds; +} + +static IntRect repaintRectForCaret(IntRect caret) +{ + if (caret.isEmpty()) + return IntRect(); + // Ensure that the dirty rect intersects the block that paints the caret even in the case where + // the caret itself is just outside the block. See <https://bugs.webkit.org/show_bug.cgi?id=19086>. + caret.inflateX(1); + return caret; +} + +IntRect SelectionController::caretRepaintRect() const +{ + return absoluteBoundsForLocalRect(repaintRectForCaret(localCaretRectForPainting())); +} + +bool SelectionController::recomputeCaretRect() +{ + if (!m_caretRectNeedsUpdate) + return false; + + if (!m_frame) + return false; + + FrameView* v = m_frame->document()->view(); + if (!v) + return false; + + IntRect oldRect = m_caretRect; + IntRect newRect = localCaretRect(); + if (oldRect == newRect && !m_absCaretBoundsDirty) + return false; + + IntRect oldAbsCaretBounds = m_absCaretBounds; + // FIXME: Rename m_caretRect to m_localCaretRect. + m_absCaretBounds = absoluteBoundsForLocalRect(m_caretRect); + m_absCaretBoundsDirty = false; + + if (oldAbsCaretBounds == m_absCaretBounds) + return false; + + IntRect oldAbsoluteCaretRepaintBounds = m_absoluteCaretRepaintBounds; + // We believe that we need to inflate the local rect before transforming it to obtain the repaint bounds. + m_absoluteCaretRepaintBounds = caretRepaintRect(); + +#if ENABLE(TEXT_CARET) + if (RenderView* view = toRenderView(m_frame->document()->renderer())) { + // FIXME: make caret repainting container-aware. + view->repaintRectangleInViewAndCompositedLayers(oldAbsoluteCaretRepaintBounds, false); + if (shouldRepaintCaret(view)) + view->repaintRectangleInViewAndCompositedLayers(m_absoluteCaretRepaintBounds, false); + } +#endif + return true; +} + +bool SelectionController::shouldRepaintCaret(const RenderView* view) const +{ + ASSERT(view); + Frame* frame = view->frameView() ? view->frameView()->frame() : 0; // The frame where the selection started. + bool caretBrowsing = frame && frame->settings() && frame->settings()->caretBrowsingEnabled(); + return (caretBrowsing || isContentEditable()); +} + +void SelectionController::invalidateCaretRect() +{ + if (!isCaret()) + return; + + Document* d = m_selection.start().node()->document(); + + // recomputeCaretRect will always return false for the drag caret, + // because its m_frame is always 0. + bool caretRectChanged = recomputeCaretRect(); + + // EDIT FIXME: This is an unfortunate hack. + // Basically, we can't trust this layout position since we + // can't guarantee that the check to see if we are in unrendered + // content will work at this point. We may have to wait for + // a layout and re-render of the document to happen. So, resetting this + // flag will cause another caret layout to happen the first time + // that we try to paint the caret after this call. That one will work since + // it happens after the document has accounted for any editing + // changes which may have been done. + // And, we need to leave this layout here so the caret moves right + // away after clicking. + m_caretRectNeedsUpdate = true; + + if (!caretRectChanged) { + RenderView* view = toRenderView(d->renderer()); + if (view && shouldRepaintCaret(view)) + view->repaintRectangleInViewAndCompositedLayers(caretRepaintRect(), false); + } +} + +void SelectionController::paintCaret(GraphicsContext* context, int tx, int ty, const IntRect& clipRect) +{ +#ifdef ANDROID_ALLOW_TURNING_OFF_CARET + if (m_frame && !android::WebViewCore::getWebViewCore(m_frame->view())->shouldPaintCaret()) + return; +#endif +#if ENABLE(TEXT_CARET) + if (!m_caretVisible) + return; + if (!m_caretPaint) + return; + if (!m_selection.isCaret()) + return; + + IntRect drawingRect = localCaretRectForPainting(); + if (caretRenderer() && caretRenderer()->isBox()) + toRenderBox(caretRenderer())->flipForWritingMode(drawingRect); + drawingRect.move(tx, ty); + IntRect caret = intersection(drawingRect, clipRect); + if (caret.isEmpty()) + return; + + Color caretColor = Color::black; + ColorSpace colorSpace = ColorSpaceDeviceRGB; + Element* element = rootEditableElement(); + if (element && element->renderer()) { + caretColor = element->renderer()->style()->visitedDependentColor(CSSPropertyColor); + colorSpace = element->renderer()->style()->colorSpace(); + } + + context->fillRect(caret, caretColor, colorSpace); +#else + UNUSED_PARAM(context); + UNUSED_PARAM(tx); + UNUSED_PARAM(ty); + UNUSED_PARAM(clipRect); +#endif +} + +void SelectionController::debugRenderer(RenderObject *r, bool selected) const +{ + if (r->node()->isElementNode()) { + Element* element = static_cast<Element *>(r->node()); + fprintf(stderr, "%s%s\n", selected ? "==> " : " ", element->localName().string().utf8().data()); + } else if (r->isText()) { + RenderText* textRenderer = toRenderText(r); + if (!textRenderer->textLength() || !textRenderer->firstTextBox()) { + fprintf(stderr, "%s#text (empty)\n", selected ? "==> " : " "); + return; + } + + static const int max = 36; + String text = textRenderer->text(); + int textLength = text.length(); + if (selected) { + int offset = 0; + if (r->node() == m_selection.start().node()) + offset = m_selection.start().deprecatedEditingOffset(); + else if (r->node() == m_selection.end().node()) + offset = m_selection.end().deprecatedEditingOffset(); + + int pos; + InlineTextBox* box = textRenderer->findNextInlineTextBox(offset, pos); + text = text.substring(box->start(), box->len()); + + String show; + int mid = max / 2; + int caret = 0; + + // text is shorter than max + if (textLength < max) { + show = text; + caret = pos; + } else if (pos - mid < 0) { + // too few characters to left + show = text.left(max - 3) + "..."; + caret = pos; + } else if (pos - mid >= 0 && pos + mid <= textLength) { + // enough characters on each side + show = "..." + text.substring(pos - mid + 3, max - 6) + "..."; + caret = mid; + } else { + // too few characters on right + show = "..." + text.right(max - 3); + caret = pos - (textLength - show.length()); + } + + show.replace('\n', ' '); + show.replace('\r', ' '); + fprintf(stderr, "==> #text : \"%s\" at offset %d\n", show.utf8().data(), pos); + fprintf(stderr, " "); + for (int i = 0; i < caret; i++) + fprintf(stderr, " "); + fprintf(stderr, "^\n"); + } else { + if ((int)text.length() > max) + text = text.left(max - 3) + "..."; + else + text = text.left(max); + fprintf(stderr, " #text : \"%s\"\n", text.utf8().data()); + } + } +} + +bool SelectionController::contains(const IntPoint& point) +{ + Document* document = m_frame->document(); + + // Treat a collapsed selection like no selection. + if (!isRange()) + return false; + if (!document->renderer()) + return false; + + HitTestRequest request(HitTestRequest::ReadOnly | + HitTestRequest::Active); + HitTestResult result(point); + document->renderView()->layer()->hitTest(request, result); + Node* innerNode = result.innerNode(); + if (!innerNode || !innerNode->renderer()) + return false; + + VisiblePosition visiblePos(innerNode->renderer()->positionForPoint(result.localPoint())); + if (visiblePos.isNull()) + return false; + + if (m_selection.visibleStart().isNull() || m_selection.visibleEnd().isNull()) + return false; + + Position start(m_selection.visibleStart().deepEquivalent()); + Position end(m_selection.visibleEnd().deepEquivalent()); + Position p(visiblePos.deepEquivalent()); + + return comparePositions(start, p) <= 0 && comparePositions(p, end) <= 0; +} + +// Workaround for the fact that it's hard to delete a frame. +// Call this after doing user-triggered selections to make it easy to delete the frame you entirely selected. +// Can't do this implicitly as part of every setSelection call because in some contexts it might not be good +// for the focus to move to another frame. So instead we call it from places where we are selecting with the +// mouse or the keyboard after setting the selection. +void SelectionController::selectFrameElementInParentIfFullySelected() +{ + // Find the parent frame; if there is none, then we have nothing to do. + Frame* parent = m_frame->tree()->parent(); + if (!parent) + return; + Page* page = m_frame->page(); + if (!page) + return; + + // Check if the selection contains the entire frame contents; if not, then there is nothing to do. + if (!isRange()) + return; + if (!isStartOfDocument(selection().visibleStart())) + return; + if (!isEndOfDocument(selection().visibleEnd())) + return; + + // Get to the <iframe> or <frame> (or even <object>) element in the parent frame. + Element* ownerElement = m_frame->document()->ownerElement(); + if (!ownerElement) + return; + ContainerNode* ownerElementParent = ownerElement->parentNode(); + if (!ownerElementParent) + return; + + // This method's purpose is it to make it easier to select iframes (in order to delete them). Don't do anything if the iframe isn't deletable. + if (!ownerElementParent->isContentEditable()) + return; + + // Create compute positions before and after the element. + unsigned ownerElementNodeIndex = ownerElement->nodeIndex(); + VisiblePosition beforeOwnerElement(VisiblePosition(ownerElementParent, ownerElementNodeIndex, SEL_DEFAULT_AFFINITY)); + VisiblePosition afterOwnerElement(VisiblePosition(ownerElementParent, ownerElementNodeIndex + 1, VP_UPSTREAM_IF_POSSIBLE)); + + // Focus on the parent frame, and then select from before this element to after. + VisibleSelection newSelection(beforeOwnerElement, afterOwnerElement); + if (parent->selection()->shouldChangeSelection(newSelection)) { + page->focusController()->setFocusedFrame(parent); + parent->selection()->setSelection(newSelection); + } +} + +void SelectionController::selectAll() +{ + Document* document = m_frame->document(); + + if (document->focusedNode() && document->focusedNode()->canSelectAll()) { + document->focusedNode()->selectAll(); + return; + } + + Node* root = 0; + if (isContentEditable()) + root = highestEditableRoot(m_selection.start()); + else { + root = shadowTreeRootNode(); + if (!root) + root = document->documentElement(); + } + if (!root) + return; + VisibleSelection newSelection(VisibleSelection::selectionFromContentsOfNode(root)); + if (shouldChangeSelection(newSelection)) + setSelection(newSelection); + selectFrameElementInParentIfFullySelected(); + notifyRendererOfSelectionChange(true); +} + +bool SelectionController::setSelectedRange(Range* range, EAffinity affinity, bool closeTyping) +{ + if (!range) + return false; + + ExceptionCode ec = 0; + Node* startContainer = range->startContainer(ec); + if (ec) + return false; + + Node* endContainer = range->endContainer(ec); + if (ec) + return false; + + ASSERT(startContainer); + ASSERT(endContainer); + ASSERT(startContainer->document() == endContainer->document()); + + m_frame->document()->updateLayoutIgnorePendingStylesheets(); + + // Non-collapsed ranges are not allowed to start at the end of a line that is wrapped, + // they start at the beginning of the next line instead + bool collapsed = range->collapsed(ec); + if (ec) + return false; + + int startOffset = range->startOffset(ec); + if (ec) + return false; + + int endOffset = range->endOffset(ec); + if (ec) + return false; + + // FIXME: Can we provide extentAffinity? + VisiblePosition visibleStart(startContainer, startOffset, collapsed ? affinity : DOWNSTREAM); + VisiblePosition visibleEnd(endContainer, endOffset, SEL_DEFAULT_AFFINITY); + setSelection(VisibleSelection(visibleStart, visibleEnd), closeTyping); + return true; +} + +bool SelectionController::isInPasswordField() const +{ + Node* startNode = start().node(); + if (!startNode) + return false; + + startNode = startNode->shadowAncestorNode(); + if (!startNode) + return false; + + if (!startNode->hasTagName(inputTag)) + return false; + + return static_cast<HTMLInputElement*>(startNode)->isPasswordField(); +} + +bool SelectionController::caretRendersInsideNode(Node* node) const +{ + if (!node) + return false; + return !isTableElement(node) && !editingIgnoresContent(node); +} + +void SelectionController::focusedOrActiveStateChanged() +{ + bool activeAndFocused = isFocusedAndActive(); + + // Because RenderObject::selectionBackgroundColor() and + // RenderObject::selectionForegroundColor() check if the frame is active, + // we have to update places those colors were painted. + if (RenderView* view = toRenderView(m_frame->document()->renderer())) + view->repaintRectangleInViewAndCompositedLayers(enclosingIntRect(bounds())); + + // Caret appears in the active frame. + if (activeAndFocused) + setSelectionFromNone(); + setCaretVisible(activeAndFocused); + + // Update for caps lock state + m_frame->eventHandler()->capsLockStateMayHaveChanged(); + + // Because CSSStyleSelector::checkOneSelector() and + // RenderTheme::isFocused() check if the frame is active, we have to + // update style and theme state that depended on those. + if (Node* node = m_frame->document()->focusedNode()) { + node->setNeedsStyleRecalc(); + if (RenderObject* renderer = node->renderer()) + if (renderer && renderer->style()->hasAppearance()) + renderer->theme()->stateChanged(renderer, FocusState); + } + + // Secure keyboard entry is set by the active frame. + if (m_frame->document()->useSecureKeyboardEntryWhenActive()) + setUseSecureKeyboardEntry(activeAndFocused); +} + +void SelectionController::pageActivationChanged() +{ + focusedOrActiveStateChanged(); +} + +void SelectionController::updateSecureKeyboardEntryIfActive() +{ + if (m_frame->document() && isFocusedAndActive()) + setUseSecureKeyboardEntry(m_frame->document()->useSecureKeyboardEntryWhenActive()); +} + +void SelectionController::setUseSecureKeyboardEntry(bool enable) +{ + if (enable) + enableSecureTextInput(); + else + disableSecureTextInput(); +} + +void SelectionController::setFocused(bool flag) +{ + if (m_focused == flag) + return; + m_focused = flag; + + focusedOrActiveStateChanged(); +} + +bool SelectionController::isFocusedAndActive() const +{ + return m_focused && m_frame->page() && m_frame->page()->focusController()->isActive(); +} + +void SelectionController::updateAppearance() +{ + ASSERT(!m_isDragCaretController); + +#if ENABLE(TEXT_CARET) + bool caretRectChanged = recomputeCaretRect(); + + bool caretBrowsing = m_frame->settings() && m_frame->settings()->caretBrowsingEnabled(); + bool shouldBlink = m_caretVisible + && isCaret() && (isContentEditable() || caretBrowsing); + + // If the caret moved, stop the blink timer so we can restart with a + // black caret in the new location. + if (caretRectChanged || !shouldBlink) + m_caretBlinkTimer.stop(); + + // Start blinking with a black caret. Be sure not to restart if we're + // already blinking in the right location. + if (shouldBlink && !m_caretBlinkTimer.isActive()) { + if (double blinkInterval = m_frame->page()->theme()->caretBlinkInterval()) + m_caretBlinkTimer.startRepeating(blinkInterval); + + if (!m_caretPaint) { + m_caretPaint = true; + invalidateCaretRect(); + } + } +#endif + + // We need to update style in case the node containing the selection is made display:none. + m_frame->document()->updateStyleIfNeeded(); + + RenderView* view = m_frame->contentRenderer(); + if (!view) + return; + + VisibleSelection selection = this->selection(); + + if (!selection.isRange()) { + view->clearSelection(); + return; + } + + // Use the rightmost candidate for the start of the selection, and the leftmost candidate for the end of the selection. + // Example: foo <a>bar</a>. Imagine that a line wrap occurs after 'foo', and that 'bar' is selected. If we pass [foo, 3] + // as the start of the selection, the selection painting code will think that content on the line containing 'foo' is selected + // and will fill the gap before 'bar'. + Position startPos = selection.start(); + Position candidate = startPos.downstream(); + if (candidate.isCandidate()) + startPos = candidate; + Position endPos = selection.end(); + candidate = endPos.upstream(); + if (candidate.isCandidate()) + endPos = candidate; + + // We can get into a state where the selection endpoints map to the same VisiblePosition when a selection is deleted + // because we don't yet notify the SelectionController of text removal. + if (startPos.isNotNull() && endPos.isNotNull() && selection.visibleStart() != selection.visibleEnd()) { + RenderObject* startRenderer = startPos.node()->renderer(); + RenderObject* endRenderer = endPos.node()->renderer(); + view->setSelection(startRenderer, startPos.deprecatedEditingOffset(), endRenderer, endPos.deprecatedEditingOffset()); + } +} + +void SelectionController::setCaretVisible(bool flag) +{ + if (m_caretVisible == flag) + return; + clearCaretRectIfNeeded(); + m_caretVisible = flag; + updateAppearance(); +} + +void SelectionController::clearCaretRectIfNeeded() +{ +#if ENABLE(TEXT_CARET) + if (!m_caretPaint) + return; + m_caretPaint = false; + invalidateCaretRect(); +#endif +} + +void SelectionController::caretBlinkTimerFired(Timer<SelectionController>*) +{ +#if ENABLE(TEXT_CARET) + ASSERT(m_caretVisible); + ASSERT(isCaret()); + bool caretPaint = m_caretPaint; + if (isCaretBlinkingSuspended() && caretPaint) + return; + m_caretPaint = !caretPaint; + invalidateCaretRect(); +#endif +} + +void SelectionController::notifyRendererOfSelectionChange(bool userTriggered) +{ + m_frame->document()->updateStyleIfNeeded(); + + if (!rootEditableElement()) + return; + + RenderObject* renderer = rootEditableElement()->shadowAncestorNode()->renderer(); + if (!renderer || !renderer->isTextControl()) + return; + + toRenderTextControl(renderer)->selectionChanged(userTriggered); +} + +// Helper function that tells whether a particular node is an element that has an entire +// Frame and FrameView, a <frame>, <iframe>, or <object>. +static bool isFrameElement(const Node* n) +{ + if (!n) + return false; + RenderObject* renderer = n->renderer(); + if (!renderer || !renderer->isWidget()) + return false; + Widget* widget = toRenderWidget(renderer)->widget(); + return widget && widget->isFrameView(); +} + +void SelectionController::setFocusedNodeIfNeeded() +{ + if (isNone() || !isFocused()) + return; + + bool caretBrowsing = m_frame->settings() && m_frame->settings()->caretBrowsingEnabled(); + if (caretBrowsing) { + if (Node* anchor = enclosingAnchorElement(base())) { + m_frame->page()->focusController()->setFocusedNode(anchor, m_frame); + return; + } + } + + if (Node* target = rootEditableElement()) { + // Walk up the DOM tree to search for a node to focus. + while (target) { + // We don't want to set focus on a subframe when selecting in a parent frame, + // so add the !isFrameElement check here. There's probably a better way to make this + // work in the long term, but this is the safest fix at this time. + if (target && target->isMouseFocusable() && !isFrameElement(target)) { + m_frame->page()->focusController()->setFocusedNode(target, m_frame); + return; + } + target = target->parentOrHostNode(); + } + m_frame->document()->setFocusedNode(0); + } + + if (caretBrowsing) + m_frame->page()->focusController()->setFocusedNode(0, m_frame); +} + +void SelectionController::paintDragCaret(GraphicsContext* p, int tx, int ty, const IntRect& clipRect) const +{ +#if ENABLE(TEXT_CARET) + SelectionController* dragCaretController = m_frame->page()->dragCaretController(); + ASSERT(dragCaretController->selection().isCaret()); + if (dragCaretController->selection().start().node()->document()->frame() == m_frame) + dragCaretController->paintCaret(p, tx, ty, clipRect); +#else + UNUSED_PARAM(p); + UNUSED_PARAM(tx); + UNUSED_PARAM(ty); + UNUSED_PARAM(clipRect); +#endif +} + +PassRefPtr<CSSMutableStyleDeclaration> SelectionController::copyTypingStyle() const +{ + if (!m_typingStyle || !m_typingStyle->style()) + return 0; + return m_typingStyle->style()->copy(); +} + +bool SelectionController::shouldDeleteSelection(const VisibleSelection& selection) const +{ + return m_frame->editor()->client()->shouldDeleteRange(selection.toNormalizedRange().get()); +} + +FloatRect SelectionController::bounds(bool clipToVisibleContent) const +{ + RenderView* root = m_frame->contentRenderer(); + FrameView* view = m_frame->view(); + if (!root || !view) + return IntRect(); + + IntRect selectionRect = root->selectionBounds(clipToVisibleContent); + return clipToVisibleContent ? intersection(selectionRect, view->visibleContentRect()) : selectionRect; +} + +void SelectionController::getClippedVisibleTextRectangles(Vector<FloatRect>& rectangles) const +{ + RenderView* root = m_frame->contentRenderer(); + if (!root) + return; + + FloatRect visibleContentRect = m_frame->view()->visibleContentRect(); + + Vector<FloatQuad> quads; + toNormalizedRange()->textQuads(quads, true); + + // FIXME: We are appending empty rectangles to the list for those that fall outside visibleContentRect. + // It might be better to omit those rectangles entirely. + size_t size = quads.size(); + for (size_t i = 0; i < size; ++i) + rectangles.append(intersection(quads[i].enclosingBoundingBox(), visibleContentRect)); +} + +// Scans logically forward from "start", including any child frames. +static HTMLFormElement* scanForForm(Node* start) +{ + for (Node* node = start; node; node = node->traverseNextNode()) { + if (node->hasTagName(formTag)) + return static_cast<HTMLFormElement*>(node); + if (node->isHTMLElement() && static_cast<HTMLElement*>(node)->isFormControlElement()) + return static_cast<HTMLFormControlElement*>(node)->form(); + if (node->hasTagName(frameTag) || node->hasTagName(iframeTag)) { + Node* childDocument = static_cast<HTMLFrameElementBase*>(node)->contentDocument(); + if (HTMLFormElement* frameResult = scanForForm(childDocument)) + return frameResult; + } + } + return 0; +} + +// We look for either the form containing the current focus, or for one immediately after it +HTMLFormElement* SelectionController::currentForm() const +{ + // Start looking either at the active (first responder) node, or where the selection is. + Node* start = m_frame->document()->focusedNode(); + if (!start) + start = this->start().node(); + + // Try walking up the node tree to find a form element. + Node* node; + for (node = start; node; node = node->parentNode()) { + if (node->hasTagName(formTag)) + return static_cast<HTMLFormElement*>(node); + if (node->isHTMLElement() && static_cast<HTMLElement*>(node)->isFormControlElement()) + return static_cast<HTMLFormControlElement*>(node)->form(); + } + + // Try walking forward in the node tree to find a form element. + return scanForForm(start); +} + +void SelectionController::revealSelection(const ScrollAlignment& alignment, bool revealExtent) +{ + IntRect rect; + + switch (selectionType()) { + case VisibleSelection::NoSelection: + return; + case VisibleSelection::CaretSelection: + rect = absoluteCaretBounds(); + break; + case VisibleSelection::RangeSelection: + rect = revealExtent ? VisiblePosition(extent()).absoluteCaretBounds() : enclosingIntRect(bounds(false)); + break; + } + + Position start = this->start(); + ASSERT(start.node()); + if (start.node() && start.node()->renderer()) { + // FIXME: This code only handles scrolling the startContainer's layer, but + // the selection rect could intersect more than just that. + // See <rdar://problem/4799899>. + if (RenderLayer* layer = start.node()->renderer()->enclosingLayer()) { + layer->scrollRectToVisible(rect, false, alignment, alignment); + updateAppearance(); + } + } +} + +void SelectionController::setSelectionFromNone() +{ + // Put a caret inside the body if the entire frame is editable (either the + // entire WebView is editable or designMode is on for this document). + + Document* document = m_frame->document(); + bool caretBrowsing = m_frame->settings() && m_frame->settings()->caretBrowsingEnabled(); + if (!isNone() || !(m_frame->isContentEditable() || caretBrowsing)) + return; + + Node* node = document->documentElement(); + while (node && !node->hasTagName(bodyTag)) + node = node->traverseNextNode(); + if (node) + setSelection(VisibleSelection(Position(node, 0), DOWNSTREAM)); +} + +bool SelectionController::shouldChangeSelection(const VisibleSelection& newSelection) const +{ + return m_frame->editor()->shouldChangeSelection(selection(), newSelection, newSelection.affinity(), false); +} + +#ifndef NDEBUG + +void SelectionController::formatForDebugger(char* buffer, unsigned length) const +{ + m_selection.formatForDebugger(buffer, length); +} + +void SelectionController::showTreeForThis() const +{ + m_selection.showTreeForThis(); +} + +#endif + +} + +#ifndef NDEBUG + +void showTree(const WebCore::SelectionController& sel) +{ + sel.showTreeForThis(); +} + +void showTree(const WebCore::SelectionController* sel) +{ + if (sel) + sel->showTreeForThis(); +} + +#endif |