blob: 5d5d0f8854b232ae5e5d8ff88be24c42b2621ceb [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/FrameCaret.h"
#include "core/editing/GranularityStrategy.h"
#include "core/editing/InputMethodController.h"
#include "core/editing/PendingSelection.h"
#include "core/editing/SelectionController.h"
#include "core/editing/SelectionEditor.h"
#include "core/editing/SelectionModifier.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/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/page/SpatialNavigation.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/PtrUtil.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->editor().behavior().shouldConsiderSelectionAsDirectional();
}
FrameSelection::FrameSelection(LocalFrame* frame)
: m_frame(frame),
m_pendingSelection(PendingSelection::create(*this)),
m_selectionEditor(SelectionEditor::create(*this)),
m_granularity(CharacterGranularity),
m_xPosForVerticalArrowNavigation(NoXPosForVerticalArrowNavigation()),
m_focused(frame->page() &&
frame->page()->focusController().focusedFrame() == frame),
m_frameCaret(new FrameCaret(frame, *m_selectionEditor)) {
DCHECK(frame);
}
FrameSelection::~FrameSelection() {}
const Document& FrameSelection::document() const {
DCHECK(m_document);
return *m_document;
}
Document& FrameSelection::document() {
DCHECK(m_document);
return *m_document;
}
// 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 : 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::moveCaretSelection(const IntPoint& point) {
DCHECK(!document().needsLayoutTreeUpdate());
Element* const editable = rootEditableElement();
if (!editable)
return;
const VisiblePosition position =
visiblePositionForContentsPoint(point, frame());
SelectionInDOMTree::Builder builder;
builder.setIsDirectional(selection().isDirectional());
if (position.isNotNull())
builder.collapse(position.toPositionWithAffinity());
setSelection(builder.build(), CloseTyping | ClearTypingStyle | UserTriggered);
}
template <typename Strategy>
void FrameSelection::setSelectionAlgorithm(
const VisibleSelectionTemplate<Strategy>& newSelection,
SetSelectionOptions options,
CursorAlignOnScroll align,
TextGranularity granularity) {
DCHECK(isAvailable());
DCHECK(newSelection.isValidFor(document()));
const Document& currentDocument = document();
if (m_granularityStrategy &&
(options & FrameSelection::DoNotClearStrategy) == 0)
m_granularityStrategy->Clear();
bool closeTyping = options & CloseTyping;
bool shouldClearTypingStyle = options & ClearTypingStyle;
EUserTriggered userTriggered = selectionOptionsToUserTriggered(options);
// TODO(editing-dev): We should rename variable |s| to another name to avoid
// using one letter variable name.
VisibleSelectionTemplate<Strategy> s = newSelection;
if (shouldAlwaysUseDirectionalSelection(m_frame))
s.setIsDirectional(true);
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 Position& oldSelectionStart = selection().start();
m_selectionEditor->setVisibleSelection(s, options);
m_frameCaret->setCaretRectNeedsUpdate();
if (!s.isNone() && !(options & DoNotSetFocus)) {
setFocusedNodeIfNeeded();
// |setFocusedNodeIfNeeded()| dispatches sync events "FocusOut" and
// "FocusIn", |m_frame| may associate to another document.
if (!isAvailable() || document() != currentDocument) {
// Once we get test case to reach here, we should change this
// if-statement to |DCHECK()|.
NOTREACHED();
return;
}
}
if (!(options & DoNotUpdateAppearance)) {
// Hits in
// compositing/overflow/do-not-paint-outline-into-composited-scrolling-contents.html
DisableCompositingQueryAsserts disabler;
m_frameCaret->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_xPosForVerticalArrowNavigation = NoXPosForVerticalArrowNavigation();
// This may dispatch a synchronous focus-related events.
selectFrameElementInParentIfFullySelected();
if (!isAvailable() || document() != currentDocument) {
// editing/selection/selectallchildren-crash.html and
// editing/selection/longpress-selection-in-iframe-removed-crash.html
// reach here.
return;
}
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(oldSelectionStart, 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));
}
// TODO(yosin): We will make |selectionInDOMTree| version of |SetSelection()|
// as primary function instead of wrapper.
void FrameSelection::setSelection(const SelectionInDOMTree& newSelection,
SetSelectionOptions options,
CursorAlignOnScroll align,
TextGranularity granularity) {
if (!newSelection.isNone()) {
// TODO(editing-dev): The use of
// updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
newSelection.base()
.document()
->updateStyleAndLayoutIgnorePendingStylesheets();
}
setSelection(createVisibleSelection(newSelection), options, align,
granularity);
}
// TODO(yosin): We will make |selectionInFlatTree| version of |SetSelection()|
// as primary function instead of wrapper.
void FrameSelection::setSelection(const SelectionInFlatTree& newSelection,
SetSelectionOptions options,
CursorAlignOnScroll align,
TextGranularity granularity) {
if (!newSelection.isNone()) {
// TODO(editing-dev): The use of
// updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
newSelection.base()
.document()
->updateStyleAndLayoutIgnorePendingStylesheets();
}
setSelection(createVisibleSelection(newSelection), options, align,
granularity);
}
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());
}
static Position computePositionForChildrenRemoval(const Position& position,
ContainerNode& container) {
Node* node = position.computeContainerNode();
if (container.containsIncludingHostElements(*node))
return Position::firstPositionInNode(&container);
return position;
}
void FrameSelection::nodeChildrenWillBeRemoved(ContainerNode& container) {
if (isNone() || !container.inActiveDocument())
return;
const Position& oldStart = selection().start();
const Position& newStart =
computePositionForChildrenRemoval(oldStart, container);
const Position& oldEnd = selection().end();
const Position& newEnd = computePositionForChildrenRemoval(oldEnd, container);
const Position& oldBase = selection().base();
const Position& newBase =
computePositionForChildrenRemoval(oldBase, container);
const Position& oldExtent = selection().extent();
const Position& newExtent =
computePositionForChildrenRemoval(oldExtent, container);
if (newStart == oldStart && newEnd == oldEnd && newBase == oldBase &&
newExtent == oldExtent)
return;
if (selection().isBaseFirst())
m_selectionEditor->setWithoutValidation(newStart, newEnd);
else
m_selectionEditor->setWithoutValidation(newEnd, newStart);
m_frameCaret->setCaretRectNeedsUpdate();
if (document().isRunningExecCommand())
return;
TypingCommand::closeTyping(m_frame);
}
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()));
m_frameCaret->nodeWillBeRemoved(node);
}
static SelectionState selectionStateOf(const Node& node) {
const LayoutObject* layoutObject = node.layoutObject();
if (!layoutObject)
return SelectionNone;
return layoutObject->getSelectionState();
}
void FrameSelection::respondToNodeModification(Node& node,
bool baseRemoved,
bool extentRemoved,
bool startRemoved,
bool endRemoved) {
DCHECK(node.document().isActive()) << node;
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 (selectionStateOf(node) != SelectionNone) {
// When node to be removed is part of selection, we invalidate
// selection to paint again.
// TODO(yosin): We should paint changed area only rather than whole
// selected range.
clearLayoutTreeSelection = true;
}
if (clearLayoutTreeSelection)
selection().start().document()->layoutViewItem().clearSelection();
if (clearDOMTreeSelection)
setSelection(VisibleSelection(), DoNotSetFocus);
m_frameCaret->setCaretRectNeedsUpdate();
// TODO(yosin): We should move to call |TypingCommand::closeTyping()| to
// |Editor| class.
if (!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
DCHECK_GE(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->isConnected())
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;
DCHECK_GE(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.isConnected())
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
DCHECK_GE(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.isConnected())
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 (!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();
}
static DispatchEventResult dispatchSelectStart(
const VisibleSelection& selection) {
Node* selectStartTarget = selection.extent().computeContainerNode();
if (!selectStartTarget)
return DispatchEventResult::NotCanceled;
return selectStartTarget->dispatchEvent(
Event::createCancelableBubble(EventTypeNames::selectstart));
}
// The return value of |FrameSelection::modify()| is different based on
// value of |userTriggered| parameter.
// When |userTriggered| is |userTriggered|, |modify()| returns false if
// "selectstart" event is dispatched and canceled, otherwise returns true.
// When |userTriggered| is |NotUserTrigged|, return value specifies whether
// selection is modified or not.
bool FrameSelection::modify(EAlteration alter,
SelectionDirection direction,
TextGranularity granularity,
EUserTriggered userTriggered) {
SelectionModifier selectionModifier(*frame(), selection(),
m_xPosForVerticalArrowNavigation);
const bool modified = selectionModifier.modify(alter, direction, granularity);
if (userTriggered == UserTriggered &&
selectionModifier.selection().isRange() && selection().isCaret() &&
dispatchSelectStart(selection()) != DispatchEventResult::NotCanceled) {
return false;
}
if (!modified) {
if (userTriggered == NotUserTriggered)
return false;
// If spatial navigation enabled, focus navigator will move focus to
// another element. See snav-input.html and snav-textarea.html
if (isSpatialNavigationEnabled(m_frame))
return false;
// Even if selection isn't changed, we prevent to default action, e.g.
// scroll window when caret is at end of content editable.
return true;
}
const SetSelectionOptions options =
CloseTyping | ClearTypingStyle | userTriggered;
setSelection(selectionModifier.selection(), options);
if (granularity == LineGranularity || granularity == ParagraphGranularity)
m_xPosForVerticalArrowNavigation =
selectionModifier.xPosForVerticalArrowNavigation();
if (userTriggered == UserTriggered)
m_granularity = CharacterGranularity;
m_frameCaret->setCaretRectNeedsUpdate();
return true;
}
bool FrameSelection::modify(EAlteration alter,
unsigned verticalDistance,
VerticalDirection direction,
EUserTriggered userTriggered,
CursorAlignOnScroll align) {
SelectionModifier selectionModifier(*frame(), selection());
if (!selectionModifier.modifyWithPageGranularity(alter, verticalDistance,
direction))
return false;
const SetSelectionOptions options =
CloseTyping | ClearTypingStyle | userTriggered;
if (alter == AlterationMove)
setSelection(selectionModifier.selection(), options, align);
else
setSelection(selectionModifier.selection(), options);
if (userTriggered == UserTriggered)
m_granularity = CharacterGranularity;
return true;
}
void FrameSelection::clear() {
m_granularity = CharacterGranularity;
if (m_granularityStrategy)
m_granularityStrategy->Clear();
setSelection(VisibleSelection());
}
void FrameSelection::documentAttached(Document* document) {
DCHECK(document);
DCHECK(!m_document) << "FrameSelection is already attached to " << m_document;
m_document = document;
m_selectionEditor->documentAttached(document);
}
void FrameSelection::documentDetached(const Document& document) {
DCHECK_EQ(m_document, document);
m_document = nullptr;
m_granularity = CharacterGranularity;
LayoutViewItem view = m_frame->contentLayoutItem();
if (!view.isNull())
view.clearSelection();
clearTypingStyle();
m_selectionEditor->documentDetached(document);
m_frameCaret->documentDetached();
m_frame->eventHandler().selectionController().documentDetached();
}
LayoutBlock* FrameSelection::caretLayoutObject() const {
DCHECK(selection().isValidFor(document()));
if (!isCaret())
return nullptr;
return CaretBase::caretLayoutObject(selection().start().anchorNode());
}
IntRect FrameSelection::absoluteCaretBounds() {
DCHECK(selection().isValidFor(*m_frame->document()));
return m_frameCaret->absoluteCaretBounds();
}
void FrameSelection::invalidateCaretRect(bool forceInvalidation) {
m_frameCaret->invalidateCaretRect(forceInvalidation);
}
void FrameSelection::dataWillChange(const CharacterData& node) {
m_frameCaret->dataWillChange(node);
}
void FrameSelection::paintCaret(GraphicsContext& context,
const LayoutPoint& paintOffset) {
m_frameCaret->paintCaret(context, paintOffset);
}
bool FrameSelection::contains(const LayoutPoint& point) {
if (document().layoutViewItem().isNull())
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().layoutViewItem().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;
// TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
document().updateStyleAndLayoutIgnorePendingStylesheets();
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;
// TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
ownerElementParent->document().updateStyleAndLayoutIgnorePendingStylesheets();
// 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 (!blink::hasEditableStyle(*ownerElementParent))
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);
SelectionInDOMTree::Builder builder;
builder
.setBaseAndExtentDeprecated(beforeOwnerElement.deepEquivalent(),
afterOwnerElement.deepEquivalent())
.setAffinity(beforeOwnerElement.affinity());
// Focus on the parent frame, and then select from before this element to
// after.
VisibleSelection newSelection = createVisibleSelection(builder.build());
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);
}
// Returns a shadow tree node for legacy shadow trees, a child of the
// ShadowRoot node for new shadow trees, or 0 for non-shadow trees.
static Node* nonBoundaryShadowTreeRootNode(const Position& position) {
return position.anchorNode() && !position.anchorNode()->isShadowRoot()
? position.anchorNode()->nonBoundaryShadowTreeRootNode()
: nullptr;
}
void FrameSelection::selectAll() {
if (isHTMLSelectElement(document().focusedElement())) {
HTMLSelectElement* selectElement =
toHTMLSelectElement(document().focusedElement());
if (selectElement->canSelectAll()) {
selectElement->selectAll();
return;
}
}
Node* root = nullptr;
Node* selectStartTarget = nullptr;
if (isContentEditable()) {
root = highestEditableRoot(selection().start());
if (Node* shadowRoot = nonBoundaryShadowTreeRootNode(selection().start()))
selectStartTarget = shadowRoot->ownerShadowHost();
else
selectStartTarget = root;
} else {
root = nonBoundaryShadowTreeRootNode(selection().start());
if (root) {
selectStartTarget = root->ownerShadowHost();
} else {
root = document().documentElement();
selectStartTarget = document().body();
}
}
if (!root || editingIgnoresContent(*root))
return;
if (selectStartTarget) {
const Document& expectedDocument = document();
if (selectStartTarget->dispatchEvent(Event::createCancelableBubble(
EventTypeNames::selectstart)) != DispatchEventResult::NotCanceled)
return;
// |root| may be detached due to selectstart event.
if (!root->isConnected() || expectedDocument != root->document())
return;
}
setSelection(SelectionInDOMTree::Builder().selectAllChildren(*root).build());
selectFrameElementInParentIfFullySelected();
notifyLayoutObjectOfSelectionChange(UserTriggered);
}
bool FrameSelection::setSelectedRange(const EphemeralRange& range,
TextAffinity affinity,
SelectionDirectionalMode directional,
SetSelectionOptions options) {
if (range.isNull())
return false;
m_selectionEditor->resetLogicalRange();
// Since |FrameSeleciton::setSelection()| dispatches events and DOM tree
// can be modified by event handlers, we should create |Range| object before
// calling it.
Range* logicalRange = createRange(range);
setSelection(SelectionInDOMTree::Builder()
.setBaseAndExtent(range)
.setAffinity(affinity)
.setIsDirectional(directional ==
SelectionDirectionalMode::Directional)
.build(),
options);
m_selectionEditor->setLogicalRange(logicalRange);
return true;
}
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 = 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();
// 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().updateStyleAndLayoutTree();
// Because LayoutObject::selectionBackgroundColor() and
// LayoutObject::selectionForegroundColor() check if the frame is active,
// we have to update places those colors were painted.
LayoutViewItem view = document().layoutViewItem();
if (!view.isNull())
view.invalidatePaintForSelection();
// Caret appears in the active frame.
if (activeAndFocused)
setSelectionFromNone();
else
m_frame->spellChecker().spellCheckAfterBlur();
m_frameCaret->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 (isFocusedAndActive())
setUseSecureKeyboardEntry(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();
}
bool FrameSelection::isAppearanceDirty() const {
return m_pendingSelection->hasPendingSelection();
}
void FrameSelection::commitAppearanceIfNeeded(LayoutView& layoutView) {
return m_pendingSelection->commit(layoutView);
}
void FrameSelection::updateAppearance() {
m_frameCaret->updateAppearance();
if (m_frame->contentLayoutItem().isNull())
return;
m_pendingSelection->setHasPendingSelection();
}
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;
if (Element* target = rootEditableElement()) {
// Walk up the DOM tree to search for a node to focus.
document().updateStyleAndLayoutTreeIgnorePendingStylesheets();
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();
}
document().clearFocusedElement();
}
}
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 extractSelectedText(*this, TextIteratorDefaultBehavior);
}
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 = 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) {
DCHECK(isAvailable());
// TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
// Calculation of absolute caret bounds requires clean layout.
document().updateStyleAndLayoutIgnorePendingStylesheets();
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();
DCHECK(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();
if (!isNone() || !(blink::hasEditableStyle(*document)))
return;
Element* documentElement = document->documentElement();
if (!documentElement)
return;
if (HTMLBodyElement* body =
Traversal<HTMLBodyElement>::firstChild(*documentElement)) {
// TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
document->updateStyleAndLayoutIgnorePendingStylesheets();
setSelection(SelectionInDOMTree::Builder()
.collapse(firstPositionInOrBeforeNode(body))
.build());
}
}
// TODO(yoichio): We should have LocalFrame having FrameCaret,
// Editor and PendingSelection using FrameCaret directly
// and get rid of this.
bool FrameSelection::shouldShowBlockCursor() const {
return m_frameCaret->shouldShowBlockCursor();
}
// TODO(yoichio): We should have LocalFrame having FrameCaret,
// Editor and PendingSelection using FrameCaret directly
// and get rid of this.
// TODO(yoichio): We should use "caret-shape" in "CSS Basic User Interface
// Module Level 4" https://drafts.csswg.org/css-ui-4/
// To use "caret-shape", we need to expose inserting mode information to CSS;
// https://github.com/w3c/csswg-drafts/issues/133
void FrameSelection::setShouldShowBlockCursor(bool shouldShowBlockCursor) {
m_frameCaret->setShouldShowBlockCursor(shouldShowBlockCursor);
}
#ifndef NDEBUG
void FrameSelection::showTreeForThis() const {
selection().showTreeForThis();
}
#endif
DEFINE_TRACE(FrameSelection) {
visitor->trace(m_document);
visitor->trace(m_frame);
visitor->trace(m_pendingSelection);
visitor->trace(m_selectionEditor);
visitor->trace(m_typingStyle);
visitor->trace(m_frameCaret);
}
void FrameSelection::scheduleVisualUpdate() const {
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(
createVisibleSelection(SelectionInDOMTree::Builder()
.collapse(start.toPositionWithAffinity())
.extend(end.deepEquivalent())
.build()),
CloseTyping | ClearTypingStyle, CursorAlignOnScroll::IfNeeded,
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 = wrapUnique(new DirectionGranularityStrategy());
else
m_granularityStrategy = wrapUnique(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);
}
// TODO(yosin): We should make |FrameSelection::moveRangeSelection()| to take
// two |IntPoint| instead of two |VisiblePosition| like
// |moveRangeSelectionExtent()|.
void FrameSelection::moveRangeSelection(const VisiblePosition& basePosition,
const VisiblePosition& extentPosition,
TextGranularity granularity) {
VisibleSelection newSelection = createVisibleSelection(
SelectionInDOMTree::Builder()
.setBaseAndExtentDeprecated(basePosition.deepEquivalent(),
extentPosition.deepEquivalent())
.setAffinity(basePosition.affinity())
.build());
newSelection.expandUsingGranularity(granularity);
if (newSelection.isNone())
return;
setSelection(newSelection, CloseTyping | ClearTypingStyle,
CursorAlignOnScroll::IfNeeded, granularity);
}
void FrameSelection::updateIfNeeded() {
DCHECK(!m_frame->document()->needsLayoutTreeUpdate());
m_selectionEditor->updateIfNeeded();
}
void FrameSelection::setCaretVisible(bool caretIsVisible) {
m_frameCaret->setCaretVisibility(caretIsVisible ? CaretVisibility::Visible
: CaretVisibility::Hidden);
}
bool FrameSelection::shouldPaintCaretForTesting() const {
return m_frameCaret->shouldPaintCaretForTesting();
}
bool FrameSelection::isPreviousCaretDirtyForTesting() const {
return m_frameCaret->isPreviousCaretDirtyForTesting();
}
bool FrameSelection::isCaretBoundsDirty() const {
return m_frameCaret->isCaretBoundsDirty();
}
void FrameSelection::setCaretRectNeedsUpdate() {
m_frameCaret->setCaretRectNeedsUpdate();
}
void FrameSelection::setCaretBlinkingSuspended(bool suspended) {
m_frameCaret->setCaretBlinkingSuspended(suspended);
}
bool FrameSelection::isCaretBlinkingSuspended() const {
return m_frameCaret->isCaretBlinkingSuspended();
}
} // namespace blink
#ifndef NDEBUG
void showTree(const blink::FrameSelection& sel) {
sel.showTreeForThis();
}
void showTree(const blink::FrameSelection* sel) {
if (sel)
sel->showTreeForThis();
else
LOG(INFO) << "Cannot showTree for <null> FrameSelection.";
}
#endif