blob: 9145e6ebc59a472df3d7da203b6b813ab6435d6b [file] [log] [blame]
/*
* 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 "core/editing/FrameSelection.h"
#include "bindings/core/v8/ExceptionState.h"
#include "core/HTMLNames.h"
#include "core/InputTypeNames.h"
#include "core/css/StylePropertySet.h"
#include "core/dom/AXObjectCache.h"
#include "core/dom/CharacterData.h"
#include "core/dom/Document.h"
#include "core/dom/Element.h"
#include "core/dom/ElementTraversal.h"
#include "core/dom/NodeTraversal.h"
#include "core/dom/Text.h"
#include "core/editing/CaretBase.h"
#include "core/editing/EditingUtilities.h"
#include "core/editing/Editor.h"
#include "core/editing/GranularityStrategy.h"
#include "core/editing/InputMethodController.h"
#include "core/editing/PendingSelection.h"
#include "core/editing/RenderedPosition.h"
#include "core/editing/SelectionController.h"
#include "core/editing/SelectionEditor.h"
#include "core/editing/TextAffinity.h"
#include "core/editing/VisibleUnits.h"
#include "core/editing/commands/TypingCommand.h"
#include "core/editing/iterators/TextIterator.h"
#include "core/editing/serializers/Serialization.h"
#include "core/editing/spellcheck/SpellChecker.h"
#include "core/events/Event.h"
#include "core/frame/FrameView.h"
#include "core/frame/LocalDOMWindow.h"
#include "core/frame/LocalFrame.h"
#include "core/frame/Settings.h"
#include "core/html/HTMLBodyElement.h"
#include "core/html/HTMLFormElement.h"
#include "core/html/HTMLFrameElementBase.h"
#include "core/html/HTMLInputElement.h"
#include "core/html/HTMLSelectElement.h"
#include "core/input/EventHandler.h"
#include "core/layout/HitTestRequest.h"
#include "core/layout/HitTestResult.h"
#include "core/layout/LayoutPart.h"
#include "core/layout/LayoutTheme.h"
#include "core/layout/api/LayoutViewItem.h"
#include "core/loader/DocumentLoader.h"
#include "core/page/EditorClient.h"
#include "core/page/FocusController.h"
#include "core/page/FrameTree.h"
#include "core/page/Page.h"
#include "core/paint/PaintLayer.h"
#include "platform/SecureTextInput.h"
#include "platform/geometry/FloatQuad.h"
#include "platform/graphics/GraphicsContext.h"
#include "platform/text/UnicodeUtilities.h"
#include "wtf/text/CString.h"
#include <stdio.h>
#define EDIT_DEBUG 0
namespace blink {
using namespace HTMLNames;
static inline bool shouldAlwaysUseDirectionalSelection(LocalFrame* frame)
{
return !frame || frame->editor().behavior().shouldConsiderSelectionAsDirectional();
}
FrameSelection::FrameSelection(LocalFrame* frame)
: m_frame(frame)
, m_pendingSelection(PendingSelection::create(*this))
, m_selectionEditor(SelectionEditor::create(*this))
, m_granularity(CharacterGranularity)
, m_previousCaretVisibility(CaretVisibility::Hidden)
, m_caretBlinkTimer(this, &FrameSelection::caretBlinkTimerFired)
, m_caretRectDirty(true)
, m_shouldPaintCaret(true)
, m_isCaretBlinkingSuspended(false)
, m_focused(frame && frame->page() && frame->page()->focusController().focusedFrame() == frame)
, m_shouldShowBlockCursor(false)
, m_caretBase(adoptPtr(new CaretBase))
{
if (shouldAlwaysUseDirectionalSelection(m_frame))
m_selectionEditor->setIsDirectional(true);
}
FrameSelection::~FrameSelection()
{
}
template <>
VisiblePosition FrameSelection::originalBase<EditingStrategy>() const
{
return m_originalBase;
}
template <>
VisiblePositionInFlatTree FrameSelection::originalBase<EditingInFlatTreeStrategy>() const
{
return m_originalBaseInFlatTree;
}
// TODO(yosin): To avoid undefined symbols in clang, we explicitly
// have specialized version of |FrameSelection::visibleSelection<Strategy>|
// before |FrameSelection::selection()| which refers this.
template <>
const VisibleSelection& FrameSelection::visibleSelection<EditingStrategy>() const
{
return m_selectionEditor->visibleSelection<EditingStrategy>();
}
template <>
const VisibleSelectionInFlatTree& FrameSelection::visibleSelection<EditingInFlatTreeStrategy>() const
{
return m_selectionEditor->visibleSelection<EditingInFlatTreeStrategy>();
}
Element* FrameSelection::rootEditableElementOrDocumentElement() const
{
Element* selectionRoot = selection().rootEditableElement();
return selectionRoot ? selectionRoot : m_frame->document()->documentElement();
}
ContainerNode* FrameSelection::rootEditableElementOrTreeScopeRootNode() const
{
Element* selectionRoot = selection().rootEditableElement();
if (selectionRoot)
return selectionRoot;
Node* node = selection().base().computeContainerNode();
return node ? &node->treeScope().rootNode() : 0;
}
const VisibleSelection& FrameSelection::selection() const
{
return visibleSelection<EditingStrategy>();
}
const VisibleSelectionInFlatTree& FrameSelection::selectionInFlatTree() const
{
return visibleSelection<EditingInFlatTreeStrategy>();
}
void FrameSelection::moveTo(const VisiblePosition &pos, EUserTriggered userTriggered, CursorAlignOnScroll align)
{
SetSelectionOptions options = CloseTyping | ClearTypingStyle | userTriggered;
setSelection(VisibleSelection(pos, pos, selection().isDirectional()), options, align);
}
void FrameSelection::moveTo(const VisiblePosition &base, const VisiblePosition &extent, EUserTriggered userTriggered)
{
const bool selectionHasDirection = true;
SetSelectionOptions options = CloseTyping | ClearTypingStyle | userTriggered;
setSelection(VisibleSelection(base, extent, selectionHasDirection), options);
}
void FrameSelection::moveTo(const Position &pos, TextAffinity affinity, EUserTriggered userTriggered)
{
SetSelectionOptions options = CloseTyping | ClearTypingStyle | userTriggered;
setSelection(VisibleSelection(pos, affinity, selection().isDirectional()), options);
}
template <typename Strategy>
static void adjustEndpointsAtBidiBoundary(VisiblePositionTemplate<Strategy>& visibleBase, VisiblePositionTemplate<Strategy>& visibleExtent)
{
RenderedPosition base(visibleBase);
RenderedPosition extent(visibleExtent);
if (base.isNull() || extent.isNull() || base.isEquivalent(extent))
return;
if (base.atLeftBoundaryOfBidiRun()) {
if (!extent.atRightBoundaryOfBidiRun(base.bidiLevelOnRight())
&& base.isEquivalent(extent.leftBoundaryOfBidiRun(base.bidiLevelOnRight()))) {
visibleBase = createVisiblePosition(fromPositionInDOMTree<Strategy>(base.positionAtLeftBoundaryOfBiDiRun()));
return;
}
return;
}
if (base.atRightBoundaryOfBidiRun()) {
if (!extent.atLeftBoundaryOfBidiRun(base.bidiLevelOnLeft())
&& base.isEquivalent(extent.rightBoundaryOfBidiRun(base.bidiLevelOnLeft()))) {
visibleBase = createVisiblePosition(fromPositionInDOMTree<Strategy>(base.positionAtRightBoundaryOfBiDiRun()));
return;
}
return;
}
if (extent.atLeftBoundaryOfBidiRun() && extent.isEquivalent(base.leftBoundaryOfBidiRun(extent.bidiLevelOnRight()))) {
visibleExtent = createVisiblePosition(fromPositionInDOMTree<Strategy>(extent.positionAtLeftBoundaryOfBiDiRun()));
return;
}
if (extent.atRightBoundaryOfBidiRun() && extent.isEquivalent(base.rightBoundaryOfBidiRun(extent.bidiLevelOnLeft()))) {
visibleExtent = createVisiblePosition(fromPositionInDOMTree<Strategy>(extent.positionAtRightBoundaryOfBiDiRun()));
return;
}
}
template <typename Strategy>
void FrameSelection::setNonDirectionalSelectionIfNeededAlgorithm(const VisibleSelectionTemplate<Strategy>& passedNewSelection, TextGranularity granularity,
EndPointsAdjustmentMode endpointsAdjustmentMode)
{
VisibleSelectionTemplate<Strategy> newSelection = passedNewSelection;
bool isDirectional = shouldAlwaysUseDirectionalSelection(m_frame) || newSelection.isDirectional();
const VisiblePositionTemplate<Strategy> originalBase = this->originalBase<Strategy>();
const VisiblePositionTemplate<Strategy> base = originalBase.isNotNull() ? originalBase : createVisiblePosition(newSelection.base());
VisiblePositionTemplate<Strategy> newBase = base;
const VisiblePositionTemplate<Strategy> extent = createVisiblePosition(newSelection.extent());
VisiblePositionTemplate<Strategy> newExtent = extent;
if (endpointsAdjustmentMode == AdjustEndpointsAtBidiBoundary)
adjustEndpointsAtBidiBoundary(newBase, newExtent);
if (newBase.deepEquivalent() != base.deepEquivalent() || newExtent.deepEquivalent() != extent.deepEquivalent()) {
setOriginalBase(base);
newSelection.setBase(newBase);
newSelection.setExtent(newExtent);
} else if (originalBase.isNotNull()) {
if (visibleSelection<Strategy>().base() == newSelection.base())
newSelection.setBase(originalBase);
setOriginalBase(VisiblePositionTemplate<Strategy>());
}
// Adjusting base and extent will make newSelection always directional
newSelection.setIsDirectional(isDirectional);
if (visibleSelection<Strategy>() == newSelection)
return;
setSelection(newSelection, granularity);
}
void FrameSelection::setNonDirectionalSelectionIfNeeded(const VisibleSelection& passedNewSelection, TextGranularity granularity, EndPointsAdjustmentMode endpointsAdjustmentMode)
{
setNonDirectionalSelectionIfNeededAlgorithm<EditingStrategy>(passedNewSelection, granularity, endpointsAdjustmentMode);
}
void FrameSelection::setNonDirectionalSelectionIfNeeded(const VisibleSelectionInFlatTree& passedNewSelection, TextGranularity granularity, EndPointsAdjustmentMode endpointsAdjustmentMode)
{
setNonDirectionalSelectionIfNeededAlgorithm<EditingInFlatTreeStrategy>(passedNewSelection, granularity, endpointsAdjustmentMode);
}
template <typename Strategy>
void FrameSelection::setSelectionAlgorithm(const VisibleSelectionTemplate<Strategy>& newSelection, SetSelectionOptions options, CursorAlignOnScroll align, TextGranularity granularity)
{
if (m_granularityStrategy && (options & FrameSelection::DoNotClearStrategy) == 0)
m_granularityStrategy->Clear();
bool closeTyping = options & CloseTyping;
bool shouldClearTypingStyle = options & ClearTypingStyle;
EUserTriggered userTriggered = selectionOptionsToUserTriggered(options);
VisibleSelectionTemplate<Strategy> s = validateSelection(newSelection);
if (shouldAlwaysUseDirectionalSelection(m_frame))
s.setIsDirectional(true);
if (!m_frame) {
m_selectionEditor->setVisibleSelection(s, options);
return;
}
// <http://bugs.webkit.org/show_bug.cgi?id=23464>: Infinite recursion at
// |FrameSelection::setSelection|
// if |document->frame()| == |m_frame| we can get into an infinite loop
if (s.base().anchorNode()) {
Document& document = *s.base().document();
// TODO(hajimehoshi): validateSelection already checks if the selection
// is valid, thus we don't need this 'if' clause any more.
if (document.frame() && document.frame() != m_frame && document != m_frame->document()) {
RawPtr<LocalFrame> guard(document.frame());
document.frame()->selection().setSelection(s, options, align, granularity);
// It's possible that during the above set selection, this
// |FrameSelection| has been modified by
// |selectFrameElementInParentIfFullySelected|, but that the
// selection is no longer valid since the frame is about to be
// destroyed. If this is the case, clear our selection.
if (!guard->host() && !selection().isNonOrphanedCaretOrRange())
clear();
return;
}
}
m_granularity = granularity;
// TODO(yosin): We should move to call |TypingCommand::closeTyping()| to
// |Editor| class.
if (closeTyping)
TypingCommand::closeTyping(m_frame);
if (shouldClearTypingStyle)
clearTypingStyle();
if (m_selectionEditor->visibleSelection<Strategy>() == s) {
// Even if selection was not changed, selection offsets may have been
// changed.
m_frame->inputMethodController().cancelCompositionIfSelectionIsInvalid();
notifyLayoutObjectOfSelectionChange(userTriggered);
return;
}
const VisibleSelectionTemplate<Strategy> oldSelection = visibleSelection<Strategy>();
const VisibleSelection oldSelectionInDOMTree = selection();
m_selectionEditor->setVisibleSelection(s, options);
setCaretRectNeedsUpdate();
if (!s.isNone() && !(options & DoNotSetFocus))
setFocusedNodeIfNeeded();
if (!(options & DoNotUpdateAppearance)) {
// Hits in compositing/overflow/do-not-paint-outline-into-composited-scrolling-contents.html
DisableCompositingQueryAsserts disabler;
stopCaretBlinkTimer();
updateAppearance();
}
// Always clear the x position used for vertical arrow navigation.
// It will be restored by the vertical arrow navigation code if necessary.
m_selectionEditor->resetXPosForVerticalArrowNavigation();
RawPtr<LocalFrame> protector(m_frame.get());
// This may dispatch a synchronous focus-related events.
selectFrameElementInParentIfFullySelected();
notifyLayoutObjectOfSelectionChange(userTriggered);
// If the selections are same in the DOM tree but not in the flat tree,
// don't fire events. For example, if the selection crosses shadow tree
// boundary, selection for the DOM tree is shrunk while that for the
// flat tree is not. Additionally, this case occurs in some edge cases.
// See also: editing/pasteboard/4076267-3.html
if (oldSelection == m_selectionEditor->visibleSelection<Strategy>()) {
m_frame->inputMethodController().cancelCompositionIfSelectionIsInvalid();
return;
}
m_frame->editor().respondToChangedSelection(oldSelectionInDOMTree, options);
if (userTriggered == UserTriggered) {
ScrollAlignment alignment;
if (m_frame->editor().behavior().shouldCenterAlignWhenSelectionIsRevealed())
alignment = (align == CursorAlignOnScroll::Always) ? ScrollAlignment::alignCenterAlways : ScrollAlignment::alignCenterIfNeeded;
else
alignment = (align == CursorAlignOnScroll::Always) ? ScrollAlignment::alignTopAlways : ScrollAlignment::alignToEdgeIfNeeded;
revealSelection(alignment, RevealExtent);
}
notifyAccessibilityForSelectionChange();
notifyCompositorForSelectionChange();
notifyEventHandlerForSelectionChange();
m_frame->localDOMWindow()->enqueueDocumentEvent(Event::create(EventTypeNames::selectionchange));
}
void FrameSelection::setSelection(const VisibleSelection& newSelection, SetSelectionOptions options, CursorAlignOnScroll align, TextGranularity granularity)
{
setSelectionAlgorithm<EditingStrategy>(newSelection, options, align, granularity);
}
void FrameSelection::setSelection(const VisibleSelectionInFlatTree& newSelection, SetSelectionOptions options, CursorAlignOnScroll align, TextGranularity granularity)
{
setSelectionAlgorithm<EditingInFlatTreeStrategy>(newSelection, options, align, granularity);
}
static bool removingNodeRemovesPosition(Node& node, const Position& position)
{
if (!position.anchorNode())
return false;
if (position.anchorNode() == node)
return true;
if (!node.isElementNode())
return false;
Element& element = toElement(node);
return element.isShadowIncludingInclusiveAncestorOf(position.anchorNode());
}
void FrameSelection::nodeWillBeRemoved(Node& node)
{
// 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 (isNone() || !node.inActiveDocument())
return;
respondToNodeModification(node, removingNodeRemovesPosition(node, selection().base()), removingNodeRemovesPosition(node, selection().extent()),
removingNodeRemovesPosition(node, selection().start()), removingNodeRemovesPosition(node, selection().end()));
if (node == m_previousCaretNode) {
// Hits in ManualTests/caret-paint-after-last-text-is-removed.html
DisableCompositingQueryAsserts disabler;
m_caretBase->invalidateLocalCaretRect(m_previousCaretNode.get(), m_previousCaretRect);
m_previousCaretNode = nullptr;
m_previousCaretRect = LayoutRect();
m_previousCaretVisibility = CaretVisibility::Hidden;
}
}
static bool intersectsNode(const VisibleSelection& selection, Node* node)
{
if (selection.isNone())
return false;
Position start = selection.start().parentAnchoredEquivalent();
Position end = selection.end().parentAnchoredEquivalent();
TrackExceptionState exceptionState;
// TODO(yosin) We should avoid to use |Range::intersectsNode()|.
return Range::intersectsNode(node, start, end, exceptionState) && !exceptionState.hadException();
}
void FrameSelection::respondToNodeModification(Node& node, bool baseRemoved, bool extentRemoved, bool startRemoved, bool endRemoved)
{
ASSERT(node.document().isActive());
bool clearLayoutTreeSelection = false;
bool clearDOMTreeSelection = false;
if (startRemoved || endRemoved) {
Position start = selection().start();
Position end = selection().end();
if (startRemoved)
updatePositionForNodeRemoval(start, node);
if (endRemoved)
updatePositionForNodeRemoval(end, node);
if (Position::commonAncestorTreeScope(start, end) && start.isNotNull() && end.isNotNull()) {
if (selection().isBaseFirst())
m_selectionEditor->setWithoutValidation(start, end);
else
m_selectionEditor->setWithoutValidation(end, start);
} else {
clearDOMTreeSelection = true;
}
clearLayoutTreeSelection = 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 (selection().isBaseFirst())
m_selectionEditor->setWithoutValidation(selection().start(), selection().end());
else
m_selectionEditor->setWithoutValidation(selection().end(), selection().start());
} else if (intersectsNode(selection(), &node)) {
// If we did nothing here, when this node's layoutObject 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.
clearLayoutTreeSelection = true;
}
if (clearLayoutTreeSelection)
selection().start().document()->layoutView()->clearSelection();
if (clearDOMTreeSelection)
setSelection(VisibleSelection(), DoNotSetFocus);
// TODO(yosin): We should move to call |TypingCommand::closeTyping()| to
// |Editor| class.
if (!m_frame->document()->isRunningExecCommand())
TypingCommand::closeTyping(m_frame);
}
static Position updatePositionAfterAdoptingTextReplacement(const Position& position, CharacterData* node, unsigned offset, unsigned oldLength, unsigned newLength)
{
if (!position.anchorNode() || position.anchorNode() != node || !position.isOffsetInAnchor())
return position;
// See: http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Mutation
ASSERT(position.offsetInContainerNode() >= 0);
unsigned positionOffset = static_cast<unsigned>(position.offsetInContainerNode());
// Replacing text can be viewed as a deletion followed by insertion.
if (positionOffset >= offset && positionOffset <= offset + oldLength)
positionOffset = offset;
// Adjust the offset if the position is after the end of the deleted contents
// (positionOffset > offset + oldLength) to avoid having a stale offset.
if (positionOffset > offset + oldLength)
positionOffset = positionOffset - oldLength + newLength;
// Due to case folding (http://unicode.org/Public/UCD/latest/ucd/CaseFolding.txt),
// LayoutText length may be different from Text length. A correct implementation
// would translate the LayoutText offset to a Text offset; this is just a safety
// precaution to avoid offset values that run off the end of the Text.
if (positionOffset > node->length())
positionOffset = node->length();
// CharacterNode in VisibleSelection must be Text node, because Comment
// and ProcessingInstruction node aren't visible.
return Position(toText(node), positionOffset);
}
void FrameSelection::didUpdateCharacterData(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 || !node->inDocument())
return;
Position base = updatePositionAfterAdoptingTextReplacement(selection().base(), node, offset, oldLength, newLength);
Position extent = updatePositionAfterAdoptingTextReplacement(selection().extent(), node, offset, oldLength, newLength);
Position start = updatePositionAfterAdoptingTextReplacement(selection().start(), node, offset, oldLength, newLength);
Position end = updatePositionAfterAdoptingTextReplacement(selection().end(), node, offset, oldLength, newLength);
updateSelectionIfNeeded(base, extent, start, end);
}
static Position updatePostionAfterAdoptingTextNodesMerged(const Position& position, const Text& oldNode, unsigned offset)
{
if (!position.anchorNode() || !position.isOffsetInAnchor())
return position;
ASSERT(position.offsetInContainerNode() >= 0);
unsigned positionOffset = static_cast<unsigned>(position.offsetInContainerNode());
if (position.anchorNode() == &oldNode)
return Position(toText(oldNode.previousSibling()), positionOffset + offset);
if (position.anchorNode() == oldNode.parentNode() && positionOffset == offset)
return Position(toText(oldNode.previousSibling()), offset);
return position;
}
void FrameSelection::didMergeTextNodes(const Text& oldNode, unsigned offset)
{
if (isNone() || !oldNode.inDocument())
return;
Position base = updatePostionAfterAdoptingTextNodesMerged(selection().base(), oldNode, offset);
Position extent = updatePostionAfterAdoptingTextNodesMerged(selection().extent(), oldNode, offset);
Position start = updatePostionAfterAdoptingTextNodesMerged(selection().start(), oldNode, offset);
Position end = updatePostionAfterAdoptingTextNodesMerged(selection().end(), oldNode, offset);
updateSelectionIfNeeded(base, extent, start, end);
}
static Position updatePostionAfterAdoptingTextNodeSplit(const Position& position, const Text& oldNode)
{
if (!position.anchorNode() || position.anchorNode() != &oldNode || !position.isOffsetInAnchor())
return position;
// See: http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Mutation
ASSERT(position.offsetInContainerNode() >= 0);
unsigned positionOffset = static_cast<unsigned>(position.offsetInContainerNode());
unsigned oldLength = oldNode.length();
if (positionOffset <= oldLength)
return position;
return Position(toText(oldNode.nextSibling()), positionOffset - oldLength);
}
void FrameSelection::didSplitTextNode(const Text& oldNode)
{
if (isNone() || !oldNode.inDocument())
return;
Position base = updatePostionAfterAdoptingTextNodeSplit(selection().base(), oldNode);
Position extent = updatePostionAfterAdoptingTextNodeSplit(selection().extent(), oldNode);
Position start = updatePostionAfterAdoptingTextNodeSplit(selection().start(), oldNode);
Position end = updatePostionAfterAdoptingTextNodeSplit(selection().end(), oldNode);
updateSelectionIfNeeded(base, extent, start, end);
}
void FrameSelection::updateSelectionIfNeeded(const Position& base, const Position& extent, const Position& start, const Position& end)
{
if (base == selection().base() && extent == selection().extent() && start == selection().start() && end == selection().end())
return;
// TODO(yosin): We should move to call |TypingCommand::closeTyping()| to
// |Editor| class.
if (!m_frame->document()->isRunningExecCommand())
TypingCommand::closeTyping(m_frame);
VisibleSelection newSelection;
if (selection().isBaseFirst())
newSelection.setWithoutValidation(start, end);
else
newSelection.setWithoutValidation(end, start);
setSelection(newSelection, DoNotSetFocus);
}
void FrameSelection::didChangeFocus()
{
// Hits in virtual/gpu/compositedscrolling/scrollbars/scrollbar-miss-mousemove-disabled.html
DisableCompositingQueryAsserts disabler;
updateAppearance();
}
bool FrameSelection::modify(EAlteration alter, SelectionDirection direction, TextGranularity granularity, EUserTriggered userTriggered)
{
if (!m_selectionEditor->modify(alter, direction, granularity, userTriggered))
return false;
if (userTriggered == UserTriggered)
m_granularity = CharacterGranularity;
setCaretRectNeedsUpdate();
return true;
}
bool FrameSelection::modify(EAlteration alter, unsigned verticalDistance, VerticalDirection direction, EUserTriggered userTriggered, CursorAlignOnScroll align)
{
if (!m_selectionEditor->modify(alter, verticalDistance, direction, userTriggered, align))
return false;
if (userTriggered == UserTriggered)
m_granularity = CharacterGranularity;
return true;
}
void FrameSelection::clear()
{
m_granularity = CharacterGranularity;
if (m_granularityStrategy)
m_granularityStrategy->Clear();
setSelection(VisibleSelection());
}
void FrameSelection::prepareForDestruction()
{
m_granularity = CharacterGranularity;
m_caretBlinkTimer.stop();
LayoutViewItem view = m_frame->contentLayoutItem();
if (!view.isNull())
view.clearSelection();
setSelection(VisibleSelection(), CloseTyping | ClearTypingStyle | DoNotUpdateAppearance);
m_selectionEditor->dispose();
m_previousCaretNode.clear();
}
void FrameSelection::setStart(const VisiblePosition &pos, EUserTriggered trigger)
{
if (selection().isBaseFirst())
setBase(pos, trigger);
else
setExtent(pos, trigger);
}
void FrameSelection::setEnd(const VisiblePosition &pos, EUserTriggered trigger)
{
if (selection().isBaseFirst())
setExtent(pos, trigger);
else
setBase(pos, trigger);
}
void FrameSelection::setBase(const VisiblePosition &pos, EUserTriggered userTriggered)
{
const bool selectionHasDirection = true;
setSelection(VisibleSelection(pos.deepEquivalent(), selection().extent(), pos.affinity(), selectionHasDirection), CloseTyping | ClearTypingStyle | userTriggered);
}
void FrameSelection::setExtent(const VisiblePosition &pos, EUserTriggered userTriggered)
{
const bool selectionHasDirection = true;
setSelection(VisibleSelection(selection().base(), pos.deepEquivalent(), pos.affinity(), selectionHasDirection), CloseTyping | ClearTypingStyle | userTriggered);
}
static bool isTextFormControl(const VisibleSelection& selection)
{
return enclosingTextFormControl(selection.start());
}
LayoutBlock* FrameSelection::caretLayoutObject() const
{
ASSERT(selection().isValidFor(*m_frame->document()));
if (!isCaret())
return nullptr;
return CaretBase::caretLayoutObject(selection().start().anchorNode());
}
IntRect FrameSelection::absoluteCaretBounds()
{
ASSERT(selection().isValidFor(*m_frame->document()));
ASSERT(m_frame->document()->lifecycle().state() != DocumentLifecycle::InPaintInvalidation);
m_frame->document()->updateLayoutIgnorePendingStylesheets();
if (!isCaret()) {
m_caretBase->clearCaretRect();
} else {
if (isTextFormControl(selection()))
m_caretBase->updateCaretRect(PositionWithAffinity(isVisuallyEquivalentCandidate(selection().start()) ? selection().start() : Position(), selection().affinity()));
else
m_caretBase->updateCaretRect(createVisiblePosition(selection().start(), selection().affinity()));
}
return m_caretBase->absoluteBoundsForLocalRect(selection().start().anchorNode(), m_caretBase->localCaretRectWithoutUpdate());
}
void FrameSelection::invalidateCaretRect()
{
if (!m_caretRectDirty)
return;
m_caretRectDirty = false;
ASSERT(selection().isValidFor(*m_frame->document()));
LayoutObject* layoutObject = nullptr;
LayoutRect newRect;
if (selection().isCaret())
newRect = localCaretRectOfPosition(PositionWithAffinity(selection().start(), selection().affinity()), layoutObject);
Node* newNode = layoutObject ? layoutObject->node() : nullptr;
if (!m_caretBlinkTimer.isActive()
&& newNode == m_previousCaretNode
&& newRect == m_previousCaretRect
&& m_caretBase->getCaretVisibility() == m_previousCaretVisibility)
return;
LayoutView* view = m_frame->document()->layoutView();
if (m_previousCaretNode && (m_caretBase->shouldRepaintCaret(*m_previousCaretNode) || m_caretBase->shouldRepaintCaret(view)))
m_caretBase->invalidateLocalCaretRect(m_previousCaretNode.get(), m_previousCaretRect);
if (newNode && (m_caretBase->shouldRepaintCaret(*newNode) || m_caretBase->shouldRepaintCaret(view)))
m_caretBase->invalidateLocalCaretRect(newNode, newRect);
m_previousCaretNode = newNode;
m_previousCaretRect = newRect;
m_previousCaretVisibility = m_caretBase->getCaretVisibility();
}
void FrameSelection::paintCaret(GraphicsContext& context, const LayoutPoint& paintOffset)
{
if (selection().isCaret() && m_shouldPaintCaret) {
m_caretBase->updateCaretRect(PositionWithAffinity(selection().start(), selection().affinity()));
m_caretBase->paintCaret(selection().start().anchorNode(), context, paintOffset);
}
}
bool FrameSelection::contains(const LayoutPoint& point)
{
Document* document = m_frame->document();
if (!document->layoutView())
return false;
// Treat a collapsed selection like no selection.
const VisibleSelectionInFlatTree& visibleSelection = this->visibleSelection<EditingInFlatTreeStrategy>();
if (!visibleSelection.isRange())
return false;
HitTestRequest request(HitTestRequest::ReadOnly | HitTestRequest::Active);
HitTestResult result(request, point);
document->layoutView()->hitTest(result);
Node* innerNode = result.innerNode();
if (!innerNode || !innerNode->layoutObject())
return false;
const VisiblePositionInFlatTree& visiblePos = createVisiblePosition(fromPositionInDOMTree<EditingInFlatTreeStrategy>(innerNode->layoutObject()->positionForPoint(result.localPoint())));
if (visiblePos.isNull())
return false;
const VisiblePositionInFlatTree& visibleStart = visibleSelection.visibleStart();
const VisiblePositionInFlatTree& visibleEnd = visibleSelection.visibleEnd();
if (visibleStart.isNull() || visibleEnd.isNull())
return false;
const PositionInFlatTree& start = visibleStart.deepEquivalent();
const PositionInFlatTree& end = visibleEnd.deepEquivalent();
const PositionInFlatTree& pos = visiblePos.deepEquivalent();
return start.compareTo(pos) <= 0 && pos.compareTo(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 FrameSelection::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;
// FIXME: This is not yet implemented for cross-process frame relationships.
if (!parent->isLocalFrame())
return;
// Get to the <iframe> or <frame> (or even <object>) element in the parent frame.
// FIXME: Doesn't work for OOPI.
HTMLFrameOwnerElement* ownerElement = m_frame->deprecatedLocalOwner();
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->hasEditableStyle())
return;
// Create compute positions before and after the element.
unsigned ownerElementNodeIndex = ownerElement->nodeIndex();
VisiblePosition beforeOwnerElement = createVisiblePosition(Position(ownerElementParent, ownerElementNodeIndex));
VisiblePosition afterOwnerElement = createVisiblePosition(Position(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);
page->focusController().setFocusedFrame(parent);
// setFocusedFrame can dispatch synchronous focus/blur events. The document
// tree might be modified.
if (newSelection.isNonOrphanedCaretOrRange())
toLocalFrame(parent)->selection().setSelection(newSelection);
}
void FrameSelection::selectAll()
{
Document* document = m_frame->document();
if (isHTMLSelectElement(document->focusedElement())) {
HTMLSelectElement* selectElement = toHTMLSelectElement(document->focusedElement());
if (selectElement->canSelectAll()) {
selectElement->selectAll();
return;
}
}
RawPtr<Node> root = nullptr;
Node* selectStartTarget = nullptr;
if (isContentEditable()) {
root = highestEditableRoot(selection().start());
if (Node* shadowRoot = selection().nonBoundaryShadowTreeRootNode())
selectStartTarget = shadowRoot->shadowHost();
else
selectStartTarget = root.get();
} else {
root = selection().nonBoundaryShadowTreeRootNode();
if (root) {
selectStartTarget = root->shadowHost();
} else {
root = document->documentElement();
selectStartTarget = document->body();
}
}
if (!root || editingIgnoresContent(root.get()))
return;
if (selectStartTarget) {
if (selectStartTarget->dispatchEvent(Event::createCancelableBubble(EventTypeNames::selectstart)) != DispatchEventResult::NotCanceled)
return;
// |root| may be detached due to selectstart event.
if (!root->inDocument() || root->document() != document)
return;
}
VisibleSelection newSelection(VisibleSelection::selectionFromContentsOfNode(root.get()));
setSelection(newSelection);
selectFrameElementInParentIfFullySelected();
notifyLayoutObjectOfSelectionChange(UserTriggered);
}
bool FrameSelection::setSelectedRange(Range* range, TextAffinity affinity, SelectionDirectionalMode directional, SetSelectionOptions options)
{
if (!range || !range->inDocument())
return false;
ASSERT(range->startContainer()->document() == range->endContainer()->document());
return setSelectedRange(EphemeralRange(range), affinity, directional, options);
}
bool FrameSelection::setSelectedRange(const EphemeralRange& range, TextAffinity affinity, SelectionDirectionalMode directional, SetSelectionOptions options)
{
return m_selectionEditor->setSelectedRange(range, affinity, directional, options);
}
RawPtr<Range> FrameSelection::firstRange() const
{
return m_selectionEditor->firstRange();
}
bool FrameSelection::isInPasswordField() const
{
HTMLTextFormControlElement* textControl = enclosingTextFormControl(start());
return isHTMLInputElement(textControl) && toHTMLInputElement(textControl)->type() == InputTypeNames::password;
}
void FrameSelection::notifyAccessibilityForSelectionChange()
{
if (selection().start().isNotNull() && selection().end().isNotNull()) {
if (AXObjectCache* cache = m_frame->document()->existingAXObjectCache())
cache->selectionChanged(selection().start().computeContainerNode());
}
}
void FrameSelection::notifyCompositorForSelectionChange()
{
if (!RuntimeEnabledFeatures::compositedSelectionUpdateEnabled())
return;
scheduleVisualUpdate();
}
void FrameSelection::notifyEventHandlerForSelectionChange()
{
m_frame->eventHandler().selectionController().notifySelectionChanged();
}
void FrameSelection::focusedOrActiveStateChanged()
{
bool activeAndFocused = isFocusedAndActive();
RawPtr<Document> document = m_frame->document();
// Trigger style invalidation from the focused element. Even though
// the focused element hasn't changed, the evaluation of focus pseudo
// selectors are dependent on whether the frame is focused and active.
if (Element* element = document->focusedElement())
element->focusStateChanged();
document->updateLayoutTree();
// Because LayoutObject::selectionBackgroundColor() and
// LayoutObject::selectionForegroundColor() check if the frame is active,
// we have to update places those colors were painted.
if (LayoutView* view = document->layoutView())
view->invalidatePaintForSelection();
// Caret appears in the active frame.
if (activeAndFocused)
setSelectionFromNone();
else
m_frame->spellChecker().spellCheckAfterBlur();
setCaretVisibility(activeAndFocused ? CaretVisibility::Visible : CaretVisibility::Hidden);
// Update for caps lock state
m_frame->eventHandler().capsLockStateMayHaveChanged();
// Secure keyboard entry is set by the active frame.
if (document->useSecureKeyboardEntryWhenActive())
setUseSecureKeyboardEntry(activeAndFocused);
}
void FrameSelection::pageActivationChanged()
{
focusedOrActiveStateChanged();
}
void FrameSelection::updateSecureKeyboardEntryIfActive()
{
if (m_frame->document() && isFocusedAndActive())
setUseSecureKeyboardEntry(m_frame->document()->useSecureKeyboardEntryWhenActive());
}
void FrameSelection::setUseSecureKeyboardEntry(bool enable)
{
if (enable)
enableSecureTextInput();
else
disableSecureTextInput();
}
void FrameSelection::setFocused(bool flag)
{
if (m_focused == flag)
return;
m_focused = flag;
focusedOrActiveStateChanged();
}
bool FrameSelection::isFocusedAndActive() const
{
return m_focused && m_frame->page() && m_frame->page()->focusController().isActive();
}
inline static bool shouldStopBlinkingDueToTypingCommand(LocalFrame* frame)
{
return frame->editor().lastEditCommand() && frame->editor().lastEditCommand()->shouldStopCaretBlinking();
}
bool FrameSelection::isAppearanceDirty() const
{
return m_pendingSelection->hasPendingSelection();
}
void FrameSelection::commitAppearanceIfNeeded(LayoutView& layoutView)
{
return m_pendingSelection->commit(layoutView);
}
void FrameSelection::updateAppearance()
{
// Paint a block cursor instead of a caret in overtype mode unless the caret is at the end of a line (in this case
// the FrameSelection will paint a blinking caret as usual).
bool paintBlockCursor = m_shouldShowBlockCursor && selection().isCaret() && !isLogicalEndOfLine(selection().visibleEnd());
bool shouldBlink = !paintBlockCursor && shouldBlinkCaret();
// If the caret moved, stop the blink timer so we can restart with a
// black caret in the new location.
if (!shouldBlink || shouldStopBlinkingDueToTypingCommand(m_frame))
stopCaretBlinkTimer();
// 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 = LayoutTheme::theme().caretBlinkInterval())
m_caretBlinkTimer.startRepeating(blinkInterval, BLINK_FROM_HERE);
m_shouldPaintCaret = true;
setCaretRectNeedsUpdate();
}
if (m_frame->contentLayoutItem().isNull())
return;
m_pendingSelection->setHasPendingSelection();
}
void FrameSelection::setCaretVisibility(CaretVisibility visibility)
{
if (m_caretBase->getCaretVisibility() == visibility)
return;
m_caretBase->setCaretVisibility(visibility);
updateAppearance();
}
bool FrameSelection::shouldBlinkCaret() const
{
if (!m_caretBase->caretIsVisible() || !isCaret())
return false;
if (m_frame->settings() && m_frame->settings()->caretBrowsingEnabled())
return false;
Element* root = rootEditableElement();
if (!root)
return false;
Element* focusedElement = root->document().focusedElement();
if (!focusedElement)
return false;
return focusedElement->isShadowIncludingInclusiveAncestorOf(selection().start().anchorNode());
}
void FrameSelection::caretBlinkTimerFired(Timer<FrameSelection>*)
{
ASSERT(m_caretBase->caretIsVisible());
ASSERT(isCaret());
if (isCaretBlinkingSuspended() && m_shouldPaintCaret)
return;
m_shouldPaintCaret = !m_shouldPaintCaret;
setCaretRectNeedsUpdate();
}
void FrameSelection::stopCaretBlinkTimer()
{
if (m_caretBlinkTimer.isActive() || m_shouldPaintCaret)
setCaretRectNeedsUpdate();
m_shouldPaintCaret = false;
m_caretBlinkTimer.stop();
}
void FrameSelection::notifyLayoutObjectOfSelectionChange(EUserTriggered userTriggered)
{
if (HTMLTextFormControlElement* textControl = enclosingTextFormControl(start()))
textControl->selectionChanged(userTriggered == UserTriggered);
}
// Helper function that tells whether a particular node is an element that has an entire
// LocalFrame and FrameView, a <frame>, <iframe>, or <object>.
static bool isFrameElement(const Node* n)
{
if (!n)
return false;
LayoutObject* layoutObject = n->layoutObject();
if (!layoutObject || !layoutObject->isLayoutPart())
return false;
Widget* widget = toLayoutPart(layoutObject)->widget();
return widget && widget->isFrameView();
}
void FrameSelection::setFocusedNodeIfNeeded()
{
if (isNone() || !isFocused())
return;
bool caretBrowsing = m_frame->settings() && m_frame->settings()->caretBrowsingEnabled();
if (caretBrowsing) {
if (Element* anchor = enclosingAnchorElement(base())) {
m_frame->page()->focusController().setFocusedElement(anchor, m_frame);
return;
}
}
if (Element* target = rootEditableElement()) {
// Walk up the DOM tree to search for a node to focus.
m_frame->document()->updateLayoutTreeIgnorePendingStylesheets();
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->isMouseFocusable() && !isFrameElement(target)) {
m_frame->page()->focusController().setFocusedElement(target, m_frame);
return;
}
target = target->parentOrShadowHostElement();
}
m_frame->document()->clearFocusedElement();
}
if (caretBrowsing)
m_frame->page()->focusController().setFocusedElement(0, m_frame);
}
static String extractSelectedText(const FrameSelection& selection, TextIteratorBehavior behavior)
{
const VisibleSelectionInFlatTree& visibleSelection = selection.visibleSelection<EditingInFlatTreeStrategy>();
const EphemeralRangeInFlatTree& range = visibleSelection.toNormalizedEphemeralRange();
// We remove '\0' characters because they are not visibly rendered to the user.
return plainText(range, behavior).replace(0, "");
}
String FrameSelection::selectedHTMLForClipboard() const
{
const VisibleSelectionInFlatTree& visibleSelection = this->visibleSelection<EditingInFlatTreeStrategy>();
const EphemeralRangeInFlatTree& range = visibleSelection.toNormalizedEphemeralRange();
return createMarkup(range.startPosition(), range.endPosition(), AnnotateForInterchange, ConvertBlocksToInlines::NotConvert, ResolveNonLocalURLs);
}
String FrameSelection::selectedText(TextIteratorBehavior behavior) const
{
return extractSelectedText(*this, behavior);
}
String FrameSelection::selectedTextForClipboard() const
{
if (m_frame->settings() && m_frame->settings()->selectionIncludesAltImageText())
return extractSelectedText(*this, TextIteratorEmitsImageAltText);
return selectedText();
}
LayoutRect FrameSelection::bounds() const
{
FrameView* view = m_frame->view();
if (!view)
return LayoutRect();
return intersection(unclippedBounds(), LayoutRect(view->visibleContentRect()));
}
LayoutRect FrameSelection::unclippedBounds() const
{
FrameView* view = m_frame->view();
LayoutViewItem layoutView = m_frame->contentLayoutItem();
if (!view || layoutView.isNull())
return LayoutRect();
view->updateLifecycleToLayoutClean();
return LayoutRect(layoutView.selectionBounds());
}
static inline HTMLFormElement* associatedFormElement(HTMLElement& element)
{
if (isHTMLFormElement(element))
return &toHTMLFormElement(element);
return element.formOwner();
}
// Scans logically forward from "start", including any child frames.
static HTMLFormElement* scanForForm(Node* start)
{
if (!start)
return 0;
for (HTMLElement& element : Traversal<HTMLElement>::startsAt(start->isHTMLElement() ? toHTMLElement(start) : Traversal<HTMLElement>::next(*start))) {
if (HTMLFormElement* form = associatedFormElement(element))
return form;
if (isHTMLFrameElementBase(element)) {
Node* childDocument = toHTMLFrameElementBase(element).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* FrameSelection::currentForm() const
{
// Start looking either at the active (first responder) node, or where the selection is.
Node* start = m_frame->document()->focusedElement();
if (!start)
start = this->start().anchorNode();
if (!start)
return 0;
// Try walking up the node tree to find a form element.
for (HTMLElement* element = Traversal<HTMLElement>::firstAncestorOrSelf(*start); element; element = Traversal<HTMLElement>::firstAncestor(*element)) {
if (HTMLFormElement* form = associatedFormElement(*element))
return form;
}
// Try walking forward in the node tree to find a form element.
return scanForForm(start);
}
void FrameSelection::revealSelection(const ScrollAlignment& alignment, RevealExtentOption revealExtentOption)
{
LayoutRect rect;
switch (getSelectionType()) {
case NoSelection:
return;
case CaretSelection:
rect = LayoutRect(absoluteCaretBounds());
break;
case RangeSelection:
rect = LayoutRect(revealExtentOption == RevealExtent ? absoluteCaretBoundsOf(createVisiblePosition(extent())) : enclosingIntRect(unclippedBounds()));
break;
}
Position start = this->start();
ASSERT(start.anchorNode());
if (start.anchorNode() && start.anchorNode()->layoutObject()) {
// FIXME: This code only handles scrolling the startContainer's layer, but
// the selection rect could intersect more than just that.
if (DocumentLoader* documentLoader = m_frame->loader().documentLoader())
documentLoader->initialScrollState().wasScrolledByUser = true;
if (start.anchorNode()->layoutObject()->scrollRectToVisible(rect, alignment, alignment))
updateAppearance();
}
}
void FrameSelection::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() || !(document->hasEditableStyle() || caretBrowsing))
return;
Element* documentElement = document->documentElement();
if (!documentElement)
return;
if (HTMLBodyElement* body = Traversal<HTMLBodyElement>::firstChild(*documentElement))
setSelection(VisibleSelection(firstPositionInOrBeforeNode(body), TextAffinity::Downstream));
}
void FrameSelection::setShouldShowBlockCursor(bool shouldShowBlockCursor)
{
m_shouldShowBlockCursor = shouldShowBlockCursor;
m_frame->document()->updateLayoutIgnorePendingStylesheets();
updateAppearance();
}
template <typename Strategy>
VisibleSelectionTemplate<Strategy> FrameSelection::validateSelection(const VisibleSelectionTemplate<Strategy>& selection)
{
if (!m_frame || selection.isNone())
return selection;
const PositionTemplate<Strategy> base = selection.base();
const PositionTemplate<Strategy> extent = selection.extent();
bool isBaseValid = base.document() == m_frame->document();
bool isExtentValid = extent.document() == m_frame->document();
if (isBaseValid && isExtentValid)
return selection;
VisibleSelectionTemplate<Strategy> newSelection;
if (isBaseValid) {
newSelection.setWithoutValidation(base, base);
} else if (isExtentValid) {
newSelection.setWithoutValidation(extent, extent);
}
return newSelection;
}
#ifndef NDEBUG
void FrameSelection::formatForDebugger(char* buffer, unsigned length) const
{
selection().formatForDebugger(buffer, length);
}
void FrameSelection::showTreeForThis() const
{
selection().showTreeForThis();
}
#endif
DEFINE_TRACE(FrameSelection)
{
visitor->trace(m_frame);
visitor->trace(m_pendingSelection);
visitor->trace(m_selectionEditor);
visitor->trace(m_originalBase);
visitor->trace(m_originalBaseInFlatTree);
visitor->trace(m_previousCaretNode);
visitor->trace(m_typingStyle);
}
void FrameSelection::setCaretRectNeedsUpdate()
{
if (m_caretRectDirty)
return;
m_caretRectDirty = true;
scheduleVisualUpdate();
}
void FrameSelection::scheduleVisualUpdate() const
{
if (!m_frame)
return;
if (Page* page = m_frame->page())
page->animator().scheduleVisualUpdate(m_frame->localFrameRoot());
}
bool FrameSelection::selectWordAroundPosition(const VisiblePosition& position)
{
static const EWordSide wordSideList[2] = { RightWordIfOnBoundary, LeftWordIfOnBoundary };
for (EWordSide wordSide : wordSideList) {
VisiblePosition start = startOfWord(position, wordSide);
VisiblePosition end = endOfWord(position, wordSide);
String text = plainText(EphemeralRange(start.deepEquivalent(), end.deepEquivalent()));
if (!text.isEmpty() && !isSeparator(text.characterStartingAt(0))) {
setSelection(VisibleSelection(start, end), WordGranularity);
return true;
}
}
return false;
}
GranularityStrategy* FrameSelection::granularityStrategy()
{
// We do lazy initalization for m_granularityStrategy, because if we
// initialize it right in the constructor - the correct settings may not be
// set yet.
SelectionStrategy strategyType = SelectionStrategy::Character;
Settings* settings = m_frame ? m_frame->settings() : 0;
if (settings && settings->selectionStrategy() == SelectionStrategy::Direction)
strategyType = SelectionStrategy::Direction;
if (m_granularityStrategy && m_granularityStrategy->GetType() == strategyType)
return m_granularityStrategy.get();
if (strategyType == SelectionStrategy::Direction)
m_granularityStrategy = adoptPtr(new DirectionGranularityStrategy());
else
m_granularityStrategy = adoptPtr(new CharacterGranularityStrategy());
return m_granularityStrategy.get();
}
void FrameSelection::moveRangeSelectionExtent(const IntPoint& contentsPoint)
{
if (isNone())
return;
VisibleSelection newSelection = granularityStrategy()->updateExtent(contentsPoint, m_frame);
setSelection(
newSelection,
FrameSelection::CloseTyping | FrameSelection::ClearTypingStyle | FrameSelection::DoNotClearStrategy | UserTriggered,
CursorAlignOnScroll::IfNeeded,
CharacterGranularity);
}
void FrameSelection::moveRangeSelection(const VisiblePosition& basePosition, const VisiblePosition& extentPosition, TextGranularity granularity)
{
VisibleSelection newSelection(basePosition, extentPosition);
newSelection.expandUsingGranularity(granularity);
if (newSelection.isNone())
return;
setSelection(newSelection, granularity);
}
void FrameSelection::updateIfNeeded()
{
m_selectionEditor->updateIfNeeded();
}
void FrameSelection::setCaretVisible(bool caretIsVisible)
{
setCaretVisibility(caretIsVisible ? CaretVisibility::Visible : CaretVisibility::Hidden);
}
} // namespace blink
#ifndef NDEBUG
void showTree(const blink::FrameSelection& sel)
{
sel.showTreeForThis();
}
void showTree(const blink::FrameSelection* sel)
{
if (sel)
sel->showTreeForThis();
else
fprintf(stderr, "Cannot showTree for (nil) FrameSelection.\n");
}
#endif