mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
The primary goal of this change was to remove EventTarget from the
sky_engine C++ code. Since EventTarget is so core to the entire event
system that sky_engine was based on, this is a rather invasive change.
As such, it had some knock-on effects. I deleted some of the files
that were affected, and cauterised the remainder.
In many cases, a file would depend on another file that it didn't
include directly, but instead included indirectly via another file
that I deleted. When this happened, if the features that this broke
were obsolete, I sometimes just removed the features instead.
Specifically:
- removed EventTarget
- removed EventQueue, since without a target, what's a queue going to
do?
- same with EventDispatch*
- removed ExecutionContext, since it had an EventQueue and nothing
else it did was relevant to Sky anymore
- removed ActiveDOMObject, which was all about ExecutionContexts
- removed ContextLifecycleNotifier since it dependend on
ExecutionContext and ActiveDOMObject
- removed the other Lifecycle classes for consistency, and replaced
them with four booleans in the Document class
- removed some of the attributes that are no longer relevant from
IDLExtendedAttributes (ConstructorCallWith and
CallWith=ExecutionContext)
- removed the Document member on DOMDartState since we never set it to
anything but null.
- removed BuiltinSky::InstallWindow since it relied on the Document
member of DOMDartState
- removed EventHandler, EventListener, and mentions of those in
various binding scripts
- removed NewEventHandler, since we're not using that either
- removed the following interfaces from the Sky Dart API:
- EventTarget
- EventListener (since without a target, there's no way to listen)
- FocusEvent (since it's only member was an EventTarget)
- HashChangeEvent (mostly by accident, but it's defunct anyway)
- FontFace (it used ConstructorCallWith=ExecutionContext)
- changed the following interfaces of the Sky DART API:
- MediaQueryList is no longer an EventTarget
- Node is no longer an EventTarget
- Document no longer has defaultView (depended on
DOMDartState's document)
- DocumentFragment, Element, Range, and Text no longer have a
constructor (they all depended on DOMDartState's document, which
is now gone)
- Event lost its EventTarget members and path.
- Window lost its WindowTimers partial interface (it used
EventTarget and ExecutionContext a lot)
- removed numerous hacks in the bindings around features that are now
gone, like addEventListener
- removed a bunch of console logging code, since that relied on
ExecutionContext
- cauterised the wound in FontFace.cpp by removing constructors and
methods that called now-removed features
- same with MediaQuery and friends
- same with some editor features and focus-related features
- same with Document
- removed DOMTimer classes since they use ExecutionContexts
1625 lines
59 KiB
C++
1625 lines
59 KiB
C++
/*
|
|
* 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 "sky/engine/core/editing/FrameSelection.h"
|
|
|
|
#include <stdio.h>
|
|
#include "sky/engine/bindings/exception_state.h"
|
|
#include "sky/engine/core/css/StylePropertySet.h"
|
|
#include "sky/engine/core/dom/CharacterData.h"
|
|
#include "sky/engine/core/dom/Document.h"
|
|
#include "sky/engine/core/dom/Element.h"
|
|
#include "sky/engine/core/dom/ElementTraversal.h"
|
|
#include "sky/engine/core/dom/NodeTraversal.h"
|
|
#include "sky/engine/core/dom/Text.h"
|
|
#include "sky/engine/core/editing/Editor.h"
|
|
#include "sky/engine/core/editing/InputMethodController.h"
|
|
#include "sky/engine/core/editing/RenderedPosition.h"
|
|
#include "sky/engine/core/editing/SpellChecker.h"
|
|
#include "sky/engine/core/editing/TextIterator.h"
|
|
#include "sky/engine/core/editing/TypingCommand.h"
|
|
#include "sky/engine/core/editing/VisibleUnits.h"
|
|
#include "sky/engine/core/editing/htmlediting.h"
|
|
#include "sky/engine/core/events/Event.h"
|
|
#include "sky/engine/core/frame/FrameView.h"
|
|
#include "sky/engine/core/frame/LocalDOMWindow.h"
|
|
#include "sky/engine/core/frame/LocalFrame.h"
|
|
#include "sky/engine/core/frame/Settings.h"
|
|
#include "sky/engine/core/page/EditorClient.h"
|
|
#include "sky/engine/core/page/FocusController.h"
|
|
#include "sky/engine/core/page/Page.h"
|
|
#include "sky/engine/core/rendering/HitTestRequest.h"
|
|
#include "sky/engine/core/rendering/HitTestResult.h"
|
|
#include "sky/engine/core/rendering/InlineTextBox.h"
|
|
#include "sky/engine/core/rendering/RenderLayer.h"
|
|
#include "sky/engine/core/rendering/RenderText.h"
|
|
#include "sky/engine/core/rendering/RenderTheme.h"
|
|
#include "sky/engine/core/rendering/RenderView.h"
|
|
#include "sky/engine/platform/geometry/FloatQuad.h"
|
|
#include "sky/engine/platform/graphics/GraphicsContext.h"
|
|
#include "sky/engine/wtf/text/CString.h"
|
|
|
|
#define EDIT_DEBUG 0
|
|
|
|
namespace blink {
|
|
|
|
static inline LayoutUnit NoXPosForVerticalArrowNavigation()
|
|
{
|
|
return LayoutUnit::min();
|
|
}
|
|
|
|
static inline bool shouldAlwaysUseDirectionalSelection(LocalFrame* frame)
|
|
{
|
|
return !frame || frame->editor().behavior().shouldConsiderSelectionAsDirectional();
|
|
}
|
|
|
|
FrameSelection::FrameSelection(LocalFrame* frame)
|
|
: m_frame(frame)
|
|
, m_xPosForVerticalArrowNavigation(NoXPosForVerticalArrowNavigation())
|
|
, m_observingVisibleSelection(false)
|
|
, m_granularity(CharacterGranularity)
|
|
, m_caretBlinkTimer(this, &FrameSelection::caretBlinkTimerFired)
|
|
, m_shouldPaintCaret(true)
|
|
, m_isCaretBlinkingSuspended(false)
|
|
, m_focused(frame && frame->page() && frame->page()->focusController().focusedFrame() == frame)
|
|
, m_shouldShowBlockCursor(false)
|
|
{
|
|
if (shouldAlwaysUseDirectionalSelection(m_frame))
|
|
m_selection.setIsDirectional(true);
|
|
}
|
|
|
|
FrameSelection::~FrameSelection()
|
|
{
|
|
#if !ENABLE(OILPAN)
|
|
// Oilpan: No need to clear out VisibleSelection observer;
|
|
// it is finalized as a part object of FrameSelection.
|
|
stopObservingVisibleSelectionChangeIfNecessary();
|
|
#endif
|
|
}
|
|
|
|
ContainerNode* FrameSelection::rootEditableElementOrTreeScopeRootNode() const
|
|
{
|
|
Element* selectionRoot = m_selection.rootEditableElement();
|
|
if (selectionRoot)
|
|
return selectionRoot;
|
|
|
|
Node* node = m_selection.base().containerNode();
|
|
return node ? &node->treeScope().rootNode() : 0;
|
|
}
|
|
|
|
void FrameSelection::moveTo(const VisiblePosition &pos, EUserTriggered userTriggered, CursorAlignOnScroll align)
|
|
{
|
|
SetSelectionOptions options = CloseTyping | ClearTypingStyle | userTriggered;
|
|
setSelection(VisibleSelection(pos.deepEquivalent(), pos.deepEquivalent(), pos.affinity(), m_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.deepEquivalent(), extent.deepEquivalent(), base.affinity(), selectionHasDirection), options);
|
|
}
|
|
|
|
void FrameSelection::moveTo(const Position &pos, EAffinity affinity, EUserTriggered userTriggered)
|
|
{
|
|
SetSelectionOptions options = CloseTyping | ClearTypingStyle | userTriggered;
|
|
setSelection(VisibleSelection(pos, affinity, m_selection.isDirectional()), options);
|
|
}
|
|
|
|
static void adjustEndpointsAtBidiBoundary(VisiblePosition& visibleBase, VisiblePosition& 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 = VisiblePosition(base.positionAtLeftBoundaryOfBiDiRun());
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (base.atRightBoundaryOfBidiRun()) {
|
|
if (!extent.atLeftBoundaryOfBidiRun(base.bidiLevelOnLeft())
|
|
&& base.isEquivalent(extent.rightBoundaryOfBidiRun(base.bidiLevelOnLeft()))) {
|
|
visibleBase = VisiblePosition(base.positionAtRightBoundaryOfBiDiRun());
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (extent.atLeftBoundaryOfBidiRun() && extent.isEquivalent(base.leftBoundaryOfBidiRun(extent.bidiLevelOnRight()))) {
|
|
visibleExtent = VisiblePosition(extent.positionAtLeftBoundaryOfBiDiRun());
|
|
return;
|
|
}
|
|
|
|
if (extent.atRightBoundaryOfBidiRun() && extent.isEquivalent(base.rightBoundaryOfBidiRun(extent.bidiLevelOnLeft()))) {
|
|
visibleExtent = VisiblePosition(extent.positionAtRightBoundaryOfBiDiRun());
|
|
return;
|
|
}
|
|
}
|
|
|
|
void FrameSelection::setNonDirectionalSelectionIfNeeded(const VisibleSelection& passedNewSelection, TextGranularity granularity,
|
|
EndPointsAdjustmentMode endpointsAdjustmentMode)
|
|
{
|
|
VisibleSelection newSelection = passedNewSelection;
|
|
bool isDirectional = shouldAlwaysUseDirectionalSelection(m_frame) || newSelection.isDirectional();
|
|
|
|
VisiblePosition base = m_originalBase.isNotNull() ? m_originalBase : newSelection.visibleBase();
|
|
VisiblePosition newBase = base;
|
|
VisiblePosition extent = newSelection.visibleExtent();
|
|
VisiblePosition newExtent = extent;
|
|
if (endpointsAdjustmentMode == AdjustEndpointsAtBidiBoundary)
|
|
adjustEndpointsAtBidiBoundary(newBase, newExtent);
|
|
|
|
if (newBase != base || newExtent != extent) {
|
|
m_originalBase = base;
|
|
newSelection.setBase(newBase);
|
|
newSelection.setExtent(newExtent);
|
|
} else if (m_originalBase.isNotNull()) {
|
|
if (m_selection.base() == newSelection.base())
|
|
newSelection.setBase(m_originalBase);
|
|
m_originalBase.clear();
|
|
}
|
|
|
|
newSelection.setIsDirectional(isDirectional); // Adjusting base and extent will make newSelection always directional
|
|
if (m_selection == newSelection)
|
|
return;
|
|
|
|
setSelection(newSelection, granularity);
|
|
}
|
|
|
|
void FrameSelection::setSelection(const VisibleSelection& newSelection, SetSelectionOptions options, CursorAlignOnScroll align, TextGranularity granularity)
|
|
{
|
|
bool closeTyping = options & CloseTyping;
|
|
bool shouldClearTypingStyle = options & ClearTypingStyle;
|
|
EUserTriggered userTriggered = selectionOptionsToUserTriggered(options);
|
|
|
|
VisibleSelection s = validateSelection(newSelection);
|
|
if (shouldAlwaysUseDirectionalSelection(m_frame))
|
|
s.setIsDirectional(true);
|
|
|
|
if (!m_frame) {
|
|
m_selection = s;
|
|
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();
|
|
if (document.frame() && document.frame() != m_frame && document != m_frame->document()) {
|
|
RefPtr<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->hasOneRef() && !m_selection.isNonOrphanedCaretOrRange())
|
|
clear();
|
|
return;
|
|
}
|
|
}
|
|
|
|
m_granularity = granularity;
|
|
|
|
if (closeTyping)
|
|
TypingCommand::closeTyping(m_frame);
|
|
|
|
if (shouldClearTypingStyle)
|
|
clearTypingStyle();
|
|
|
|
if (m_selection == s) {
|
|
// Even if selection was not changed, selection offsets may have been changed.
|
|
m_frame->inputMethodController().cancelCompositionIfSelectionIsInvalid();
|
|
notifyRendererOfSelectionChange(userTriggered);
|
|
return;
|
|
}
|
|
|
|
VisibleSelection oldSelection = m_selection;
|
|
|
|
m_selection = s;
|
|
setCaretRectNeedsUpdate();
|
|
|
|
if (!s.isNone() && !(options & DoNotSetFocus))
|
|
setFocusedNodeIfNeeded();
|
|
|
|
if (!(options & DoNotUpdateAppearance)) {
|
|
updateAppearance(ResetCaretBlink);
|
|
}
|
|
|
|
// 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();
|
|
notifyRendererOfSelectionChange(userTriggered);
|
|
m_frame->editor().respondToChangedSelection(oldSelection, options);
|
|
if (userTriggered == UserTriggered) {
|
|
ScrollAlignment alignment = (align == AlignCursorOnScrollAlways) ? ScrollAlignment::alignTopAlways : ScrollAlignment::alignToEdgeIfNeeded;
|
|
revealSelection(alignment, RevealExtent);
|
|
}
|
|
|
|
notifyAccessibilityForSelectionChange();
|
|
}
|
|
|
|
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.containsIncludingShadowDOM(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, m_selection.base()), removingNodeRemovesPosition(node, m_selection.extent()),
|
|
removingNodeRemovesPosition(node, m_selection.start()), removingNodeRemovesPosition(node, m_selection.end()));
|
|
}
|
|
|
|
void FrameSelection::respondToNodeModification(Node& node, bool baseRemoved, bool extentRemoved, bool startRemoved, bool endRemoved)
|
|
{
|
|
ASSERT(node.document().isActive());
|
|
|
|
bool clearRenderTreeSelection = false;
|
|
bool clearDOMTreeSelection = false;
|
|
|
|
if (startRemoved || endRemoved) {
|
|
Position start = m_selection.start();
|
|
Position end = m_selection.end();
|
|
if (startRemoved)
|
|
updatePositionForNodeRemoval(start, node);
|
|
if (endRemoved)
|
|
updatePositionForNodeRemoval(end, node);
|
|
|
|
if (start.isNotNull() && end.isNotNull()) {
|
|
if (m_selection.isBaseFirst())
|
|
m_selection.setWithoutValidation(start, end);
|
|
else
|
|
m_selection.setWithoutValidation(end, start);
|
|
} else
|
|
clearDOMTreeSelection = true;
|
|
|
|
clearRenderTreeSelection = true;
|
|
} else if (baseRemoved || extentRemoved) {
|
|
// The base and/or extent are about to be removed, but the start and end aren't.
|
|
// Change the base and extent to the start and end, but don't re-validate the
|
|
// selection, since doing so could move the start and end into the node
|
|
// that is about to be removed.
|
|
if (m_selection.isBaseFirst())
|
|
m_selection.setWithoutValidation(m_selection.start(), m_selection.end());
|
|
else
|
|
m_selection.setWithoutValidation(m_selection.end(), m_selection.start());
|
|
} else if (RefPtr<Range> range = m_selection.firstRange()) {
|
|
TrackExceptionState exceptionState;
|
|
Range::CompareResults compareResult = range->compareNode(&node, exceptionState);
|
|
if (!exceptionState.had_exception() && (compareResult == Range::NODE_BEFORE_AND_AFTER || compareResult == Range::NODE_INSIDE)) {
|
|
// If we did nothing here, when this node's renderer was destroyed, the rect that it
|
|
// occupied would be invalidated, but, selection gaps that change as a result of
|
|
// the removal wouldn't be invalidated.
|
|
// FIXME: Don't do so much unnecessary invalidation.
|
|
clearRenderTreeSelection = true;
|
|
}
|
|
}
|
|
|
|
if (clearRenderTreeSelection)
|
|
m_selection.start().document()->renderView()->clearSelection();
|
|
|
|
if (clearDOMTreeSelection)
|
|
setSelection(VisibleSelection(), DoNotSetFocus);
|
|
}
|
|
|
|
static Position updatePositionAfterAdoptingTextReplacement(const Position& position, CharacterData* node, unsigned offset, unsigned oldLength, unsigned newLength)
|
|
{
|
|
if (!position.anchorNode() || position.anchorNode() != node || position.anchorType() != Position::PositionIsOffsetInAnchor)
|
|
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;
|
|
|
|
ASSERT_WITH_SECURITY_IMPLICATION(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(m_selection.base(), node, offset, oldLength, newLength);
|
|
Position extent = updatePositionAfterAdoptingTextReplacement(m_selection.extent(), node, offset, oldLength, newLength);
|
|
Position start = updatePositionAfterAdoptingTextReplacement(m_selection.start(), node, offset, oldLength, newLength);
|
|
Position end = updatePositionAfterAdoptingTextReplacement(m_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.anchorType() != Position::PositionIsOffsetInAnchor)
|
|
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(m_selection.base(), oldNode, offset);
|
|
Position extent = updatePostionAfterAdoptingTextNodesMerged(m_selection.extent(), oldNode, offset);
|
|
Position start = updatePostionAfterAdoptingTextNodesMerged(m_selection.start(), oldNode, offset);
|
|
Position end = updatePostionAfterAdoptingTextNodesMerged(m_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.anchorType() != Position::PositionIsOffsetInAnchor)
|
|
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(m_selection.base(), oldNode);
|
|
Position extent = updatePostionAfterAdoptingTextNodeSplit(m_selection.extent(), oldNode);
|
|
Position start = updatePostionAfterAdoptingTextNodeSplit(m_selection.start(), oldNode);
|
|
Position end = updatePostionAfterAdoptingTextNodeSplit(m_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 == m_selection.base() && extent == m_selection.extent() && start == m_selection.start() && end == m_selection.end())
|
|
return;
|
|
VisibleSelection newSelection;
|
|
if (m_selection.isBaseFirst())
|
|
newSelection.setWithoutValidation(start, end);
|
|
else
|
|
newSelection.setWithoutValidation(end, start);
|
|
setSelection(newSelection, DoNotSetFocus);
|
|
}
|
|
|
|
TextDirection FrameSelection::directionOfEnclosingBlock()
|
|
{
|
|
return blink::directionOfEnclosingBlock(m_selection.extent());
|
|
}
|
|
|
|
TextDirection FrameSelection::directionOfSelection()
|
|
{
|
|
InlineBox* startBox = 0;
|
|
InlineBox* endBox = 0;
|
|
int unusedOffset;
|
|
// Cache the VisiblePositions because visibleStart() and visibleEnd()
|
|
// can cause layout, which has the potential to invalidate lineboxes.
|
|
VisiblePosition startPosition = m_selection.visibleStart();
|
|
VisiblePosition endPosition = m_selection.visibleEnd();
|
|
if (startPosition.isNotNull())
|
|
startPosition.getInlineBoxAndOffset(startBox, unusedOffset);
|
|
if (endPosition.isNotNull())
|
|
endPosition.getInlineBoxAndOffset(endBox, unusedOffset);
|
|
if (startBox && endBox && startBox->direction() == endBox->direction())
|
|
return startBox->direction();
|
|
|
|
return directionOfEnclosingBlock();
|
|
}
|
|
|
|
void FrameSelection::didChangeFocus()
|
|
{
|
|
updateAppearance();
|
|
}
|
|
|
|
void FrameSelection::willBeModified(EAlteration alter, SelectionDirection direction)
|
|
{
|
|
if (alter != AlterationExtend)
|
|
return;
|
|
|
|
Position start = m_selection.start();
|
|
Position end = m_selection.end();
|
|
|
|
bool baseIsStart = true;
|
|
|
|
if (m_selection.isDirectional()) {
|
|
// Make base and extent match start and end so we extend the user-visible selection.
|
|
// This only matters for cases where base and extend point to different positions than
|
|
// start and end (e.g. after a double-click to select a word).
|
|
if (m_selection.isBaseFirst())
|
|
baseIsStart = true;
|
|
else
|
|
baseIsStart = false;
|
|
} else {
|
|
switch (direction) {
|
|
case DirectionRight:
|
|
if (directionOfSelection() == LTR)
|
|
baseIsStart = true;
|
|
else
|
|
baseIsStart = false;
|
|
break;
|
|
case DirectionForward:
|
|
baseIsStart = true;
|
|
break;
|
|
case DirectionLeft:
|
|
if (directionOfSelection() == LTR)
|
|
baseIsStart = false;
|
|
else
|
|
baseIsStart = true;
|
|
break;
|
|
case DirectionBackward:
|
|
baseIsStart = false;
|
|
break;
|
|
}
|
|
}
|
|
if (baseIsStart) {
|
|
m_selection.setBase(start);
|
|
m_selection.setExtent(end);
|
|
} else {
|
|
m_selection.setBase(end);
|
|
m_selection.setExtent(start);
|
|
}
|
|
}
|
|
|
|
VisiblePosition FrameSelection::positionForPlatform(bool isGetStart) const
|
|
{
|
|
// FIXME: VisibleSelection should be fixed to ensure as an invariant that
|
|
// base/extent always point to the same nodes as start/end, but which points
|
|
// to which depends on the value of isBaseFirst. Then this can be changed
|
|
// to just return m_sel.extent().
|
|
return m_selection.isBaseFirst() ? m_selection.visibleEnd() : m_selection.visibleStart();
|
|
}
|
|
|
|
VisiblePosition FrameSelection::startForPlatform() const
|
|
{
|
|
return positionForPlatform(true);
|
|
}
|
|
|
|
VisiblePosition FrameSelection::endForPlatform() const
|
|
{
|
|
return positionForPlatform(false);
|
|
}
|
|
|
|
VisiblePosition FrameSelection::nextWordPositionForPlatform(const VisiblePosition &originalPosition)
|
|
{
|
|
VisiblePosition positionAfterCurrentWord = nextWordPosition(originalPosition);
|
|
|
|
if (m_frame && m_frame->editor().behavior().shouldSkipSpaceWhenMovingRight()) {
|
|
// In order to skip spaces when moving right, we advance one
|
|
// word further and then move one word back. Given the
|
|
// semantics of previousWordPosition() this will put us at the
|
|
// beginning of the word following.
|
|
VisiblePosition positionAfterSpacingAndFollowingWord = nextWordPosition(positionAfterCurrentWord);
|
|
if (positionAfterSpacingAndFollowingWord.isNotNull() && positionAfterSpacingAndFollowingWord != positionAfterCurrentWord)
|
|
positionAfterCurrentWord = previousWordPosition(positionAfterSpacingAndFollowingWord);
|
|
|
|
bool movingBackwardsMovedPositionToStartOfCurrentWord = positionAfterCurrentWord == previousWordPosition(nextWordPosition(originalPosition));
|
|
if (movingBackwardsMovedPositionToStartOfCurrentWord)
|
|
positionAfterCurrentWord = positionAfterSpacingAndFollowingWord;
|
|
}
|
|
return positionAfterCurrentWord;
|
|
}
|
|
|
|
static void adjustPositionForUserSelectAll(VisiblePosition& pos, bool isForward)
|
|
{
|
|
if (Node* rootUserSelectAll = Position::rootUserSelectAllForNode(pos.deepEquivalent().anchorNode()))
|
|
pos = VisiblePosition(isForward ? positionAfterNode(rootUserSelectAll).downstream(CanCrossEditingBoundary) : positionBeforeNode(rootUserSelectAll).upstream(CanCrossEditingBoundary));
|
|
}
|
|
|
|
VisiblePosition FrameSelection::modifyExtendingRight(TextGranularity granularity)
|
|
{
|
|
VisiblePosition pos(m_selection.extent(), m_selection.affinity());
|
|
|
|
// The difference between modifyExtendingRight and modifyExtendingForward is:
|
|
// modifyExtendingForward always extends forward logically.
|
|
// modifyExtendingRight behaves the same as modifyExtendingForward except for extending character or word,
|
|
// it extends forward logically if the enclosing block is LTR direction,
|
|
// but it extends backward logically if the enclosing block is RTL direction.
|
|
switch (granularity) {
|
|
case CharacterGranularity:
|
|
if (directionOfEnclosingBlock() == LTR)
|
|
pos = pos.next(CanSkipOverEditingBoundary);
|
|
else
|
|
pos = pos.previous(CanSkipOverEditingBoundary);
|
|
break;
|
|
case WordGranularity:
|
|
if (directionOfEnclosingBlock() == LTR)
|
|
pos = nextWordPositionForPlatform(pos);
|
|
else
|
|
pos = previousWordPosition(pos);
|
|
break;
|
|
case LineBoundary:
|
|
if (directionOfEnclosingBlock() == LTR)
|
|
pos = modifyExtendingForward(granularity);
|
|
else
|
|
pos = modifyExtendingBackward(granularity);
|
|
break;
|
|
case SentenceGranularity:
|
|
case LineGranularity:
|
|
case ParagraphGranularity:
|
|
case SentenceBoundary:
|
|
case ParagraphBoundary:
|
|
case DocumentBoundary:
|
|
// FIXME: implement all of the above?
|
|
pos = modifyExtendingForward(granularity);
|
|
break;
|
|
}
|
|
adjustPositionForUserSelectAll(pos, directionOfEnclosingBlock() == LTR);
|
|
return pos;
|
|
}
|
|
|
|
VisiblePosition FrameSelection::modifyExtendingForward(TextGranularity granularity)
|
|
{
|
|
VisiblePosition pos(m_selection.extent(), m_selection.affinity());
|
|
switch (granularity) {
|
|
case CharacterGranularity:
|
|
pos = pos.next(CanSkipOverEditingBoundary);
|
|
break;
|
|
case WordGranularity:
|
|
pos = nextWordPositionForPlatform(pos);
|
|
break;
|
|
case SentenceGranularity:
|
|
pos = nextSentencePosition(pos);
|
|
break;
|
|
case LineGranularity:
|
|
pos = nextLinePosition(pos, lineDirectionPointForBlockDirectionNavigation(EXTENT));
|
|
break;
|
|
case ParagraphGranularity:
|
|
pos = nextParagraphPosition(pos, lineDirectionPointForBlockDirectionNavigation(EXTENT));
|
|
break;
|
|
case SentenceBoundary:
|
|
pos = endOfSentence(endForPlatform());
|
|
break;
|
|
case LineBoundary:
|
|
pos = logicalEndOfLine(endForPlatform());
|
|
break;
|
|
case ParagraphBoundary:
|
|
pos = endOfParagraph(endForPlatform());
|
|
break;
|
|
case DocumentBoundary:
|
|
pos = endForPlatform();
|
|
if (isEditablePosition(pos.deepEquivalent()))
|
|
pos = endOfEditableContent(pos);
|
|
else
|
|
pos = endOfDocument(pos);
|
|
break;
|
|
}
|
|
adjustPositionForUserSelectAll(pos, directionOfEnclosingBlock() == LTR);
|
|
return pos;
|
|
}
|
|
|
|
VisiblePosition FrameSelection::modifyMovingRight(TextGranularity granularity)
|
|
{
|
|
VisiblePosition pos;
|
|
switch (granularity) {
|
|
case CharacterGranularity:
|
|
if (isRange()) {
|
|
if (directionOfSelection() == LTR)
|
|
pos = VisiblePosition(m_selection.end(), m_selection.affinity());
|
|
else
|
|
pos = VisiblePosition(m_selection.start(), m_selection.affinity());
|
|
} else
|
|
pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).right(true);
|
|
break;
|
|
case WordGranularity: {
|
|
bool skipsSpaceWhenMovingRight = m_frame && m_frame->editor().behavior().shouldSkipSpaceWhenMovingRight();
|
|
pos = rightWordPosition(VisiblePosition(m_selection.extent(), m_selection.affinity()), skipsSpaceWhenMovingRight);
|
|
break;
|
|
}
|
|
case SentenceGranularity:
|
|
case LineGranularity:
|
|
case ParagraphGranularity:
|
|
case SentenceBoundary:
|
|
case ParagraphBoundary:
|
|
case DocumentBoundary:
|
|
// FIXME: Implement all of the above.
|
|
pos = modifyMovingForward(granularity);
|
|
break;
|
|
case LineBoundary:
|
|
pos = rightBoundaryOfLine(startForPlatform(), directionOfEnclosingBlock());
|
|
break;
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
VisiblePosition FrameSelection::modifyMovingForward(TextGranularity granularity)
|
|
{
|
|
VisiblePosition pos;
|
|
// FIXME: Stay in editable content for the less common granularities.
|
|
switch (granularity) {
|
|
case CharacterGranularity:
|
|
if (isRange())
|
|
pos = VisiblePosition(m_selection.end(), m_selection.affinity());
|
|
else
|
|
pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).next(CanSkipOverEditingBoundary);
|
|
break;
|
|
case WordGranularity:
|
|
pos = nextWordPositionForPlatform(VisiblePosition(m_selection.extent(), m_selection.affinity()));
|
|
break;
|
|
case SentenceGranularity:
|
|
pos = nextSentencePosition(VisiblePosition(m_selection.extent(), m_selection.affinity()));
|
|
break;
|
|
case LineGranularity: {
|
|
// down-arrowing from a range selection that ends at the start of a line needs
|
|
// to leave the selection at that line start (no need to call nextLinePosition!)
|
|
pos = endForPlatform();
|
|
if (!isRange() || !isStartOfLine(pos))
|
|
pos = nextLinePosition(pos, lineDirectionPointForBlockDirectionNavigation(START));
|
|
break;
|
|
}
|
|
case ParagraphGranularity:
|
|
pos = nextParagraphPosition(endForPlatform(), lineDirectionPointForBlockDirectionNavigation(START));
|
|
break;
|
|
case SentenceBoundary:
|
|
pos = endOfSentence(endForPlatform());
|
|
break;
|
|
case LineBoundary:
|
|
pos = logicalEndOfLine(endForPlatform());
|
|
break;
|
|
case ParagraphBoundary:
|
|
pos = endOfParagraph(endForPlatform());
|
|
break;
|
|
case DocumentBoundary:
|
|
pos = endForPlatform();
|
|
if (isEditablePosition(pos.deepEquivalent()))
|
|
pos = endOfEditableContent(pos);
|
|
else
|
|
pos = endOfDocument(pos);
|
|
break;
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
VisiblePosition FrameSelection::modifyExtendingLeft(TextGranularity granularity)
|
|
{
|
|
VisiblePosition pos(m_selection.extent(), m_selection.affinity());
|
|
|
|
// The difference between modifyExtendingLeft and modifyExtendingBackward is:
|
|
// modifyExtendingBackward always extends backward logically.
|
|
// modifyExtendingLeft behaves the same as modifyExtendingBackward except for extending character or word,
|
|
// it extends backward logically if the enclosing block is LTR direction,
|
|
// but it extends forward logically if the enclosing block is RTL direction.
|
|
switch (granularity) {
|
|
case CharacterGranularity:
|
|
if (directionOfEnclosingBlock() == LTR)
|
|
pos = pos.previous(CanSkipOverEditingBoundary);
|
|
else
|
|
pos = pos.next(CanSkipOverEditingBoundary);
|
|
break;
|
|
case WordGranularity:
|
|
if (directionOfEnclosingBlock() == LTR)
|
|
pos = previousWordPosition(pos);
|
|
else
|
|
pos = nextWordPositionForPlatform(pos);
|
|
break;
|
|
case LineBoundary:
|
|
if (directionOfEnclosingBlock() == LTR)
|
|
pos = modifyExtendingBackward(granularity);
|
|
else
|
|
pos = modifyExtendingForward(granularity);
|
|
break;
|
|
case SentenceGranularity:
|
|
case LineGranularity:
|
|
case ParagraphGranularity:
|
|
case SentenceBoundary:
|
|
case ParagraphBoundary:
|
|
case DocumentBoundary:
|
|
pos = modifyExtendingBackward(granularity);
|
|
break;
|
|
}
|
|
adjustPositionForUserSelectAll(pos, !(directionOfEnclosingBlock() == LTR));
|
|
return pos;
|
|
}
|
|
|
|
VisiblePosition FrameSelection::modifyExtendingBackward(TextGranularity granularity)
|
|
{
|
|
VisiblePosition pos(m_selection.extent(), m_selection.affinity());
|
|
|
|
// Extending a selection backward by word or character from just after a table selects
|
|
// the table. This "makes sense" from the user perspective, esp. when deleting.
|
|
// It was done here instead of in VisiblePosition because we want VPs to iterate
|
|
// over everything.
|
|
switch (granularity) {
|
|
case CharacterGranularity:
|
|
pos = pos.previous(CanSkipOverEditingBoundary);
|
|
break;
|
|
case WordGranularity:
|
|
pos = previousWordPosition(pos);
|
|
break;
|
|
case SentenceGranularity:
|
|
pos = previousSentencePosition(pos);
|
|
break;
|
|
case LineGranularity:
|
|
pos = previousLinePosition(pos, lineDirectionPointForBlockDirectionNavigation(EXTENT));
|
|
break;
|
|
case ParagraphGranularity:
|
|
pos = previousParagraphPosition(pos, lineDirectionPointForBlockDirectionNavigation(EXTENT));
|
|
break;
|
|
case SentenceBoundary:
|
|
pos = startOfSentence(startForPlatform());
|
|
break;
|
|
case LineBoundary:
|
|
pos = logicalStartOfLine(startForPlatform());
|
|
break;
|
|
case ParagraphBoundary:
|
|
pos = startOfParagraph(startForPlatform());
|
|
break;
|
|
case DocumentBoundary:
|
|
pos = startForPlatform();
|
|
if (isEditablePosition(pos.deepEquivalent()))
|
|
pos = startOfEditableContent(pos);
|
|
else
|
|
pos = startOfDocument(pos);
|
|
break;
|
|
}
|
|
adjustPositionForUserSelectAll(pos, !(directionOfEnclosingBlock() == LTR));
|
|
return pos;
|
|
}
|
|
|
|
VisiblePosition FrameSelection::modifyMovingLeft(TextGranularity granularity)
|
|
{
|
|
VisiblePosition pos;
|
|
switch (granularity) {
|
|
case CharacterGranularity:
|
|
if (isRange())
|
|
if (directionOfSelection() == LTR)
|
|
pos = VisiblePosition(m_selection.start(), m_selection.affinity());
|
|
else
|
|
pos = VisiblePosition(m_selection.end(), m_selection.affinity());
|
|
else
|
|
pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).left(true);
|
|
break;
|
|
case WordGranularity: {
|
|
bool skipsSpaceWhenMovingRight = m_frame && m_frame->editor().behavior().shouldSkipSpaceWhenMovingRight();
|
|
pos = leftWordPosition(VisiblePosition(m_selection.extent(), m_selection.affinity()), skipsSpaceWhenMovingRight);
|
|
break;
|
|
}
|
|
case SentenceGranularity:
|
|
case LineGranularity:
|
|
case ParagraphGranularity:
|
|
case SentenceBoundary:
|
|
case ParagraphBoundary:
|
|
case DocumentBoundary:
|
|
// FIXME: Implement all of the above.
|
|
pos = modifyMovingBackward(granularity);
|
|
break;
|
|
case LineBoundary:
|
|
pos = leftBoundaryOfLine(startForPlatform(), directionOfEnclosingBlock());
|
|
break;
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
VisiblePosition FrameSelection::modifyMovingBackward(TextGranularity granularity)
|
|
{
|
|
VisiblePosition pos;
|
|
switch (granularity) {
|
|
case CharacterGranularity:
|
|
if (isRange())
|
|
pos = VisiblePosition(m_selection.start(), m_selection.affinity());
|
|
else
|
|
pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).previous(CanSkipOverEditingBoundary);
|
|
break;
|
|
case WordGranularity:
|
|
pos = previousWordPosition(VisiblePosition(m_selection.extent(), m_selection.affinity()));
|
|
break;
|
|
case SentenceGranularity:
|
|
pos = previousSentencePosition(VisiblePosition(m_selection.extent(), m_selection.affinity()));
|
|
break;
|
|
case LineGranularity:
|
|
pos = previousLinePosition(startForPlatform(), lineDirectionPointForBlockDirectionNavigation(START));
|
|
break;
|
|
case ParagraphGranularity:
|
|
pos = previousParagraphPosition(startForPlatform(), lineDirectionPointForBlockDirectionNavigation(START));
|
|
break;
|
|
case SentenceBoundary:
|
|
pos = startOfSentence(startForPlatform());
|
|
break;
|
|
case LineBoundary:
|
|
pos = logicalStartOfLine(startForPlatform());
|
|
break;
|
|
case ParagraphBoundary:
|
|
pos = startOfParagraph(startForPlatform());
|
|
break;
|
|
case DocumentBoundary:
|
|
pos = startForPlatform();
|
|
if (isEditablePosition(pos.deepEquivalent()))
|
|
pos = startOfEditableContent(pos);
|
|
else
|
|
pos = startOfDocument(pos);
|
|
break;
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
static bool isBoundary(TextGranularity granularity)
|
|
{
|
|
return granularity == LineBoundary || granularity == ParagraphBoundary || granularity == DocumentBoundary;
|
|
}
|
|
|
|
bool FrameSelection::modify(EAlteration alter, SelectionDirection direction, TextGranularity granularity, EUserTriggered userTriggered)
|
|
{
|
|
if (userTriggered == UserTriggered) {
|
|
OwnPtr<FrameSelection> trialFrameSelection = FrameSelection::create();
|
|
trialFrameSelection->setSelection(m_selection);
|
|
trialFrameSelection->modify(alter, direction, granularity, NotUserTriggered);
|
|
|
|
if (trialFrameSelection->selection().isRange() && m_selection.isCaret() && !dispatchSelectStart())
|
|
return false;
|
|
}
|
|
|
|
willBeModified(alter, direction);
|
|
|
|
VisiblePosition originalStartPosition = m_selection.visibleStart();
|
|
VisiblePosition position;
|
|
switch (direction) {
|
|
case DirectionRight:
|
|
if (alter == AlterationMove)
|
|
position = modifyMovingRight(granularity);
|
|
else
|
|
position = modifyExtendingRight(granularity);
|
|
break;
|
|
case DirectionForward:
|
|
if (alter == AlterationExtend)
|
|
position = modifyExtendingForward(granularity);
|
|
else
|
|
position = modifyMovingForward(granularity);
|
|
break;
|
|
case DirectionLeft:
|
|
if (alter == AlterationMove)
|
|
position = modifyMovingLeft(granularity);
|
|
else
|
|
position = modifyExtendingLeft(granularity);
|
|
break;
|
|
case DirectionBackward:
|
|
if (alter == AlterationExtend)
|
|
position = modifyExtendingBackward(granularity);
|
|
else
|
|
position = modifyMovingBackward(granularity);
|
|
break;
|
|
}
|
|
|
|
if (position.isNull())
|
|
return false;
|
|
|
|
// Some of the above operations set an xPosForVerticalArrowNavigation.
|
|
// Setting a selection will clear it, so save it to possibly restore later.
|
|
// Note: the START position type is arbitrary because it is unused, it would be
|
|
// the requested position type if there were no xPosForVerticalArrowNavigation set.
|
|
LayoutUnit x = lineDirectionPointForBlockDirectionNavigation(START);
|
|
m_selection.setIsDirectional(shouldAlwaysUseDirectionalSelection(m_frame) || alter == AlterationExtend);
|
|
|
|
switch (alter) {
|
|
case AlterationMove:
|
|
moveTo(position, userTriggered);
|
|
break;
|
|
case AlterationExtend:
|
|
|
|
if (!m_selection.isCaret()
|
|
&& (granularity == WordGranularity || granularity == ParagraphGranularity || granularity == LineGranularity)
|
|
&& m_frame && !m_frame->editor().behavior().shouldExtendSelectionByWordOrLineAcrossCaret()) {
|
|
// Don't let the selection go across the base position directly. Needed to match mac
|
|
// behavior when, for instance, word-selecting backwards starting with the caret in
|
|
// the middle of a word and then word-selecting forward, leaving the caret in the
|
|
// same place where it was, instead of directly selecting to the end of the word.
|
|
VisibleSelection newSelection = m_selection;
|
|
newSelection.setExtent(position);
|
|
if (m_selection.isBaseFirst() != newSelection.isBaseFirst())
|
|
position = m_selection.visibleBase();
|
|
}
|
|
|
|
// Standard Mac behavior when extending to a boundary is grow the selection rather than leaving the
|
|
// base in place and moving the extent. Matches NSTextView.
|
|
if (!m_frame || !m_frame->editor().behavior().shouldAlwaysGrowSelectionWhenExtendingToBoundary() || m_selection.isCaret() || !isBoundary(granularity))
|
|
setExtent(position, userTriggered);
|
|
else {
|
|
TextDirection textDirection = directionOfEnclosingBlock();
|
|
if (direction == DirectionForward || (textDirection == LTR && direction == DirectionRight) || (textDirection == RTL && direction == DirectionLeft))
|
|
setEnd(position, userTriggered);
|
|
else
|
|
setStart(position, userTriggered);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (granularity == LineGranularity || granularity == ParagraphGranularity)
|
|
m_xPosForVerticalArrowNavigation = x;
|
|
|
|
if (userTriggered == UserTriggered)
|
|
m_granularity = CharacterGranularity;
|
|
|
|
setCaretRectNeedsUpdate();
|
|
|
|
return true;
|
|
}
|
|
|
|
// FIXME: Maybe baseline would be better?
|
|
static bool absoluteCaretY(const VisiblePosition &c, int &y)
|
|
{
|
|
IntRect rect = c.absoluteCaretBounds();
|
|
if (rect.isEmpty())
|
|
return false;
|
|
y = rect.y() + rect.height() / 2;
|
|
return true;
|
|
}
|
|
|
|
bool FrameSelection::modify(EAlteration alter, unsigned verticalDistance, VerticalDirection direction, EUserTriggered userTriggered, CursorAlignOnScroll align)
|
|
{
|
|
if (!verticalDistance)
|
|
return false;
|
|
|
|
if (userTriggered == UserTriggered) {
|
|
OwnPtr<FrameSelection> trialFrameSelection = FrameSelection::create();
|
|
trialFrameSelection->setSelection(m_selection);
|
|
trialFrameSelection->modify(alter, verticalDistance, direction, NotUserTriggered);
|
|
}
|
|
|
|
willBeModified(alter, direction == DirectionUp ? DirectionBackward : DirectionForward);
|
|
|
|
VisiblePosition pos;
|
|
LayoutUnit xPos = 0;
|
|
switch (alter) {
|
|
case AlterationMove:
|
|
pos = VisiblePosition(direction == DirectionUp ? m_selection.start() : m_selection.end(), m_selection.affinity());
|
|
xPos = lineDirectionPointForBlockDirectionNavigation(direction == DirectionUp ? START : END);
|
|
m_selection.setAffinity(direction == DirectionUp ? UPSTREAM : DOWNSTREAM);
|
|
break;
|
|
case AlterationExtend:
|
|
pos = VisiblePosition(m_selection.extent(), m_selection.affinity());
|
|
xPos = lineDirectionPointForBlockDirectionNavigation(EXTENT);
|
|
m_selection.setAffinity(DOWNSTREAM);
|
|
break;
|
|
}
|
|
|
|
int startY;
|
|
if (!absoluteCaretY(pos, startY))
|
|
return false;
|
|
if (direction == DirectionUp)
|
|
startY = -startY;
|
|
int lastY = startY;
|
|
|
|
VisiblePosition result;
|
|
VisiblePosition next;
|
|
for (VisiblePosition p = pos; ; p = next) {
|
|
if (direction == DirectionUp)
|
|
next = previousLinePosition(p, xPos);
|
|
else
|
|
next = nextLinePosition(p, xPos);
|
|
|
|
if (next.isNull() || next == p)
|
|
break;
|
|
int nextY;
|
|
if (!absoluteCaretY(next, nextY))
|
|
break;
|
|
if (direction == DirectionUp)
|
|
nextY = -nextY;
|
|
if (nextY - startY > static_cast<int>(verticalDistance))
|
|
break;
|
|
if (nextY >= lastY) {
|
|
lastY = nextY;
|
|
result = next;
|
|
}
|
|
}
|
|
|
|
if (result.isNull())
|
|
return false;
|
|
|
|
switch (alter) {
|
|
case AlterationMove:
|
|
moveTo(result, userTriggered, align);
|
|
break;
|
|
case AlterationExtend:
|
|
setExtent(result, userTriggered);
|
|
break;
|
|
}
|
|
|
|
if (userTriggered == UserTriggered)
|
|
m_granularity = CharacterGranularity;
|
|
|
|
m_selection.setIsDirectional(shouldAlwaysUseDirectionalSelection(m_frame) || alter == AlterationExtend);
|
|
|
|
return true;
|
|
}
|
|
|
|
LayoutUnit FrameSelection::lineDirectionPointForBlockDirectionNavigation(EPositionType type)
|
|
{
|
|
LayoutUnit x = 0;
|
|
|
|
if (isNone())
|
|
return x;
|
|
|
|
Position pos;
|
|
switch (type) {
|
|
case START:
|
|
pos = m_selection.start();
|
|
break;
|
|
case END:
|
|
pos = m_selection.end();
|
|
break;
|
|
case BASE:
|
|
pos = m_selection.base();
|
|
break;
|
|
case EXTENT:
|
|
pos = m_selection.extent();
|
|
break;
|
|
}
|
|
|
|
LocalFrame* frame = pos.document()->frame();
|
|
if (!frame)
|
|
return x;
|
|
|
|
if (m_xPosForVerticalArrowNavigation == NoXPosForVerticalArrowNavigation()) {
|
|
VisiblePosition visiblePosition(pos, m_selection.affinity());
|
|
// VisiblePosition creation can fail here if a node containing the selection becomes visibility:hidden
|
|
// after the selection is created and before this function is called.
|
|
x = visiblePosition.isNotNull() ? visiblePosition.lineDirectionPointForBlockDirectionNavigation() : 0;
|
|
m_xPosForVerticalArrowNavigation = x;
|
|
} else
|
|
x = m_xPosForVerticalArrowNavigation;
|
|
|
|
return x;
|
|
}
|
|
|
|
void FrameSelection::clear()
|
|
{
|
|
m_granularity = CharacterGranularity;
|
|
setSelection(VisibleSelection());
|
|
}
|
|
|
|
void FrameSelection::prepareForDestruction()
|
|
{
|
|
m_granularity = CharacterGranularity;
|
|
|
|
m_caretBlinkTimer.stop();
|
|
|
|
RenderView* view = m_frame->contentRenderer();
|
|
if (view)
|
|
view->clearSelection();
|
|
|
|
m_frame = 0;
|
|
|
|
setSelection(VisibleSelection(), CloseTyping | ClearTypingStyle | DoNotUpdateAppearance);
|
|
}
|
|
|
|
void FrameSelection::setStart(const VisiblePosition &pos, EUserTriggered trigger)
|
|
{
|
|
if (m_selection.isBaseFirst())
|
|
setBase(pos, trigger);
|
|
else
|
|
setExtent(pos, trigger);
|
|
}
|
|
|
|
void FrameSelection::setEnd(const VisiblePosition &pos, EUserTriggered trigger)
|
|
{
|
|
if (m_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(), m_selection.extent(), pos.affinity(), selectionHasDirection), CloseTyping | ClearTypingStyle | userTriggered);
|
|
}
|
|
|
|
void FrameSelection::setExtent(const VisiblePosition &pos, EUserTriggered userTriggered)
|
|
{
|
|
const bool selectionHasDirection = true;
|
|
setSelection(VisibleSelection(m_selection.base(), pos.deepEquivalent(), pos.affinity(), selectionHasDirection), CloseTyping | ClearTypingStyle | userTriggered);
|
|
}
|
|
|
|
RenderBlock* FrameSelection::caretRenderer() const
|
|
{
|
|
return CaretBase::caretRenderer(m_selection.start().deprecatedNode());
|
|
}
|
|
|
|
static bool isNonOrphanedCaret(const VisibleSelection& selection)
|
|
{
|
|
return selection.isCaret() && !selection.start().isOrphan() && !selection.end().isOrphan();
|
|
}
|
|
|
|
IntRect FrameSelection::absoluteCaretBounds()
|
|
{
|
|
m_frame->document()->updateLayout();
|
|
if (!isNonOrphanedCaret(m_selection)) {
|
|
clearCaretRect();
|
|
} else {
|
|
updateCaretRect(m_frame->document(), VisiblePosition(m_selection.start(), m_selection.affinity()));
|
|
}
|
|
return absoluteBoundsForLocalRect(m_selection.start().deprecatedNode(), localCaretRectWithoutUpdate());
|
|
}
|
|
|
|
void FrameSelection::paintCaret(GraphicsContext* context, const LayoutPoint& paintOffset, const LayoutRect& clipRect)
|
|
{
|
|
if (m_selection.isCaret() && m_shouldPaintCaret) {
|
|
updateCaretRect(m_frame->document(), PositionWithAffinity(m_selection.start(), m_selection.affinity()));
|
|
CaretBase::paintCaret(m_selection.start().deprecatedNode(), context, paintOffset, clipRect);
|
|
}
|
|
}
|
|
|
|
bool FrameSelection::contains(const LayoutPoint& point)
|
|
{
|
|
Document* document = m_frame->document();
|
|
|
|
// Treat a collapsed selection like no selection.
|
|
if (!isRange())
|
|
return false;
|
|
if (!document->renderView())
|
|
return false;
|
|
|
|
HitTestRequest request(HitTestRequest::ReadOnly | HitTestRequest::Active);
|
|
HitTestResult result(point);
|
|
document->renderView()->hitTest(request, result);
|
|
Node* innerNode = result.innerNode();
|
|
if (!innerNode || !innerNode->renderer())
|
|
return false;
|
|
|
|
VisiblePosition visiblePos(innerNode->renderer()->positionForPoint(result.localPoint()));
|
|
if (visiblePos.isNull())
|
|
return false;
|
|
|
|
if (m_selection.visibleStart().isNull() || m_selection.visibleEnd().isNull())
|
|
return false;
|
|
|
|
Position start(m_selection.visibleStart().deepEquivalent());
|
|
Position end(m_selection.visibleEnd().deepEquivalent());
|
|
Position p(visiblePos.deepEquivalent());
|
|
|
|
return comparePositions(start, p) <= 0 && comparePositions(p, end) <= 0;
|
|
}
|
|
|
|
void FrameSelection::selectAll()
|
|
{
|
|
Document* document = m_frame->document();
|
|
|
|
RefPtr<Node> root = nullptr;
|
|
Node* selectStartTarget = 0;
|
|
if (isContentEditable()) {
|
|
root = highestEditableRoot(m_selection.start());
|
|
if (Node* shadowRoot = m_selection.nonBoundaryShadowTreeRootNode())
|
|
selectStartTarget = shadowRoot->shadowHost();
|
|
else
|
|
selectStartTarget = root.get();
|
|
} else {
|
|
root = m_selection.nonBoundaryShadowTreeRootNode();
|
|
if (root) {
|
|
selectStartTarget = root->shadowHost();
|
|
} else {
|
|
root = document;
|
|
selectStartTarget = document;
|
|
}
|
|
}
|
|
if (!root)
|
|
return;
|
|
|
|
VisibleSelection newSelection(VisibleSelection::selectionFromContentsOfNode(root.get()));
|
|
setSelection(newSelection);
|
|
notifyRendererOfSelectionChange(UserTriggered);
|
|
}
|
|
|
|
bool FrameSelection::setSelectedRange(Range* range, EAffinity affinity, DirectoinalOption directional, SetSelectionOptions options)
|
|
{
|
|
if (!range || !range->startContainer() || !range->endContainer())
|
|
return false;
|
|
ASSERT(range->startContainer()->document() == range->endContainer()->document());
|
|
|
|
// Non-collapsed ranges are not allowed to start at the end of a line that is wrapped,
|
|
// they start at the beginning of the next line instead
|
|
m_logicalRange = nullptr;
|
|
stopObservingVisibleSelectionChangeIfNecessary();
|
|
|
|
VisibleSelection newSelection(range, affinity, directional == Directional);
|
|
setSelection(newSelection, options);
|
|
|
|
m_logicalRange = range->cloneRange();
|
|
startObservingVisibleSelectionChange();
|
|
|
|
return true;
|
|
}
|
|
|
|
PassRefPtr<Range> FrameSelection::firstRange() const
|
|
{
|
|
if (m_logicalRange)
|
|
return m_logicalRange->cloneRange();
|
|
return m_selection.firstRange();
|
|
}
|
|
|
|
bool FrameSelection::isInPasswordField() const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
void FrameSelection::notifyAccessibilityForSelectionChange()
|
|
{
|
|
}
|
|
|
|
void FrameSelection::focusedOrActiveStateChanged()
|
|
{
|
|
bool activeAndFocused = isFocusedAndActive();
|
|
|
|
RefPtr<Document> document = m_frame->document();
|
|
document->updateRenderTreeIfNeeded();
|
|
|
|
// Caret appears in the active frame.
|
|
if (activeAndFocused)
|
|
setSelectionFromNone();
|
|
else
|
|
m_frame->spellChecker().spellCheckAfterBlur();
|
|
setCaretVisibility(activeAndFocused ? Visible : Hidden);
|
|
|
|
// We may have lost active status even though the focusElement hasn't changed
|
|
// give the element a chance to recalc style if its affected by focus.
|
|
if (Element* element = document->focusedElement())
|
|
element->focusStateChanged();
|
|
}
|
|
|
|
void FrameSelection::pageActivationChanged()
|
|
{
|
|
focusedOrActiveStateChanged();
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
void FrameSelection::updateAppearance(ResetCaretBlinkOption option)
|
|
{
|
|
// 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 && m_selection.isCaret() && !isLogicalEndOfLine(m_selection.visibleEnd());
|
|
|
|
bool shouldBlink = !paintBlockCursor && shouldBlinkCaret();
|
|
|
|
bool willNeedCaretRectUpdate = false;
|
|
|
|
// If the caret moved, stop the blink timer so we can restart with a
|
|
// black caret in the new location.
|
|
if (option == ResetCaretBlink || !shouldBlink || shouldStopBlinkingDueToTypingCommand(m_frame)) {
|
|
m_caretBlinkTimer.stop();
|
|
|
|
m_shouldPaintCaret = false;
|
|
willNeedCaretRectUpdate = true;
|
|
}
|
|
|
|
// 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 = RenderTheme::theme().caretBlinkInterval())
|
|
m_caretBlinkTimer.startRepeating(blinkInterval, FROM_HERE);
|
|
|
|
m_shouldPaintCaret = true;
|
|
willNeedCaretRectUpdate = true;
|
|
}
|
|
|
|
if (willNeedCaretRectUpdate)
|
|
setCaretRectNeedsUpdate();
|
|
|
|
RenderView* view = m_frame->contentRenderer();
|
|
if (!view)
|
|
return;
|
|
|
|
// Construct a new VisibleSolution, since m_selection is not necessarily valid, and the following steps
|
|
// assume a valid selection. See <https://bugs.webkit.org/show_bug.cgi?id=69563> and <rdar://problem/10232866>.
|
|
|
|
VisibleSelection selection;
|
|
VisiblePosition endVisiblePosition = paintBlockCursor ? modifyExtendingForward(CharacterGranularity) : m_selection.visibleEnd();
|
|
selection = VisibleSelection(m_selection.visibleStart(), endVisiblePosition);
|
|
|
|
if (!selection.isRange()) {
|
|
view->clearSelection();
|
|
return;
|
|
}
|
|
|
|
m_frame->document()->updateLayout();
|
|
|
|
// Use the rightmost candidate for the start of the selection, and the leftmost candidate for the end of the selection.
|
|
// Example: foo <a>bar</a>. Imagine that a line wrap occurs after 'foo', and that 'bar' is selected. If we pass [foo, 3]
|
|
// as the start of the selection, the selection painting code will think that content on the line containing 'foo' is selected
|
|
// and will fill the gap before 'bar'.
|
|
Position startPos = selection.start();
|
|
Position candidate = startPos.downstream();
|
|
if (candidate.isCandidate())
|
|
startPos = candidate;
|
|
Position endPos = selection.end();
|
|
candidate = endPos.upstream();
|
|
if (candidate.isCandidate())
|
|
endPos = candidate;
|
|
|
|
// We can get into a state where the selection endpoints map to the same VisiblePosition when a selection is deleted
|
|
// because we don't yet notify the FrameSelection of text removal.
|
|
if (startPos.isNotNull() && endPos.isNotNull() && selection.visibleStart() != selection.visibleEnd()) {
|
|
RenderObject* startRenderer = startPos.deprecatedNode()->renderer();
|
|
RenderObject* endRenderer = endPos.deprecatedNode()->renderer();
|
|
if (startRenderer->view() == view && endRenderer->view() == view)
|
|
view->setSelection(startRenderer, startPos.deprecatedEditingOffset(), endRenderer, endPos.deprecatedEditingOffset());
|
|
}
|
|
}
|
|
|
|
void FrameSelection::setCaretVisibility(CaretVisibility visibility)
|
|
{
|
|
if (caretVisibility() == visibility)
|
|
return;
|
|
|
|
CaretBase::setCaretVisibility(visibility);
|
|
|
|
updateAppearance();
|
|
}
|
|
|
|
bool FrameSelection::shouldBlinkCaret() const
|
|
{
|
|
if (!caretIsVisible() || !isCaret())
|
|
return false;
|
|
|
|
Element* root = rootEditableElement();
|
|
if (!root)
|
|
return false;
|
|
|
|
Element* focusedElement = root->document().focusedElement();
|
|
if (!focusedElement)
|
|
return false;
|
|
|
|
return focusedElement->containsIncludingShadowDOM(m_selection.start().anchorNode());
|
|
}
|
|
|
|
void FrameSelection::caretBlinkTimerFired(Timer<FrameSelection>*)
|
|
{
|
|
ASSERT(caretIsVisible());
|
|
ASSERT(isCaret());
|
|
if (isCaretBlinkingSuspended() && m_shouldPaintCaret)
|
|
return;
|
|
m_shouldPaintCaret = !m_shouldPaintCaret;
|
|
setCaretRectNeedsUpdate();
|
|
}
|
|
|
|
void FrameSelection::notifyRendererOfSelectionChange(EUserTriggered 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)
|
|
{
|
|
// FIXME(sky): Remove this.
|
|
return false;
|
|
}
|
|
|
|
void FrameSelection::setFocusedNodeIfNeeded()
|
|
{
|
|
if (isNone() || !isFocused())
|
|
return;
|
|
|
|
if (Element* target = rootEditableElement()) {
|
|
// Walk up the DOM tree to search for a node to focus.
|
|
while (target) {
|
|
// We don't want to set focus on a subframe when selecting in a parent frame,
|
|
// so add the !isFrameElement check here. There's probably a better way to make this
|
|
// work in the long term, but this is the safest fix at this time.
|
|
if (target->isFocusable() && !isFrameElement(target)) {
|
|
m_frame->page()->focusController().setFocusedElement(target, m_frame);
|
|
return;
|
|
}
|
|
target = target->parentOrShadowHostElement();
|
|
}
|
|
m_frame->document()->setFocusedElement(nullptr);
|
|
}
|
|
}
|
|
|
|
String FrameSelection::selectedText() const
|
|
{
|
|
// We remove '\0' characters because they are not visibly rendered to the user.
|
|
return plainText(toNormalizedRange().get()).replace(0, "");
|
|
}
|
|
|
|
void FrameSelection::revealSelection(const ScrollAlignment& alignment, RevealExtentOption revealExtentOption)
|
|
{
|
|
LayoutRect rect;
|
|
|
|
switch (selectionType()) {
|
|
case NoSelection:
|
|
return;
|
|
case CaretSelection:
|
|
rect = absoluteCaretBounds();
|
|
break;
|
|
case RangeSelection:
|
|
Position position = revealExtentOption == RevealExtent ? extent() : base();
|
|
rect = VisiblePosition(position).absoluteCaretBounds();
|
|
break;
|
|
}
|
|
|
|
Position start = this->start();
|
|
ASSERT(start.deprecatedNode());
|
|
if (start.deprecatedNode() && start.deprecatedNode()->renderer()) {
|
|
// FIXME: This code only handles scrolling the startContainer's layer, but
|
|
// the selection rect could intersect more than just that.
|
|
// See <rdar://problem/4799899>.
|
|
|
|
// FIXME(sky): Figure out how to reveal the selection...
|
|
}
|
|
}
|
|
|
|
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).
|
|
|
|
// FIXME(sky): We have no body.
|
|
}
|
|
|
|
bool FrameSelection::dispatchSelectStart()
|
|
{
|
|
Node* selectStartTarget = m_selection.extent().containerNode();
|
|
if (!selectStartTarget)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
void FrameSelection::setShouldShowBlockCursor(bool shouldShowBlockCursor)
|
|
{
|
|
m_shouldShowBlockCursor = shouldShowBlockCursor;
|
|
|
|
m_frame->document()->updateLayout();
|
|
|
|
updateAppearance();
|
|
}
|
|
|
|
void FrameSelection::didChangeVisibleSelection()
|
|
{
|
|
ASSERT(m_observingVisibleSelection);
|
|
// Invalidate the logical range when the underlying VisibleSelection has changed.
|
|
m_logicalRange = nullptr;
|
|
m_selection.clearChangeObserver();
|
|
m_observingVisibleSelection = false;
|
|
}
|
|
|
|
VisibleSelection FrameSelection::validateSelection(const VisibleSelection& selection)
|
|
{
|
|
if (!m_frame || selection.isNone())
|
|
return selection;
|
|
|
|
Position base = selection.base();
|
|
Position extent = selection.extent();
|
|
bool isBaseValid = base.document() == m_frame->document();
|
|
bool isExtentValid = extent.document() == m_frame->document();
|
|
|
|
if (isBaseValid && isExtentValid)
|
|
return selection;
|
|
|
|
VisibleSelection newSelection;
|
|
if (isBaseValid) {
|
|
newSelection.setWithoutValidation(base, base);
|
|
} else if (isExtentValid) {
|
|
newSelection.setWithoutValidation(extent, extent);
|
|
}
|
|
return newSelection;
|
|
}
|
|
|
|
void FrameSelection::startObservingVisibleSelectionChange()
|
|
{
|
|
ASSERT(!m_observingVisibleSelection);
|
|
m_selection.setChangeObserver(*this);
|
|
m_observingVisibleSelection = true;
|
|
}
|
|
|
|
void FrameSelection::stopObservingVisibleSelectionChangeIfNecessary()
|
|
{
|
|
if (m_observingVisibleSelection) {
|
|
m_selection.clearChangeObserver();
|
|
m_observingVisibleSelection = false;
|
|
}
|
|
}
|
|
|
|
#ifndef NDEBUG
|
|
|
|
void FrameSelection::formatForDebugger(char* buffer, unsigned length) const
|
|
{
|
|
m_selection.formatForDebugger(buffer, length);
|
|
}
|
|
|
|
void FrameSelection::showTreeForThis() const
|
|
{
|
|
m_selection.showTreeForThis();
|
|
}
|
|
|
|
#endif
|
|
|
|
void FrameSelection::setCaretRectNeedsUpdate()
|
|
{
|
|
if (m_frame)
|
|
m_frame->document()->scheduleVisualUpdate();
|
|
}
|
|
|
|
}
|
|
|
|
#ifndef NDEBUG
|
|
|
|
void showTree(const blink::FrameSelection& sel)
|
|
{
|
|
sel.showTreeForThis();
|
|
}
|
|
|
|
void showTree(const blink::FrameSelection* sel)
|
|
{
|
|
if (sel)
|
|
sel->showTreeForThis();
|
|
}
|
|
|
|
#endif
|