blob: 444584d6264a490b8b8483f2cafcc927abd3bc46 [file] [log] [blame]
/*
* Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Apple Inc. All rights
* reserved.
* Copyright (C) 2006 Alexey Proskuryakov (ap@webkit.org)
* Copyright (C) 2012 Digia Plc. and/or its subsidiary(-ies)
* Copyright (C) 2015 Google 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/SelectionController.h"
#include "core/HTMLNames.h"
#include "core/dom/Document.h"
#include "core/editing/EditingUtilities.h"
#include "core/editing/Editor.h"
#include "core/editing/FrameSelection.h"
#include "core/editing/RenderedPosition.h"
#include "core/editing/iterators/TextIterator.h"
#include "core/editing/markers/DocumentMarkerController.h"
#include "core/events/Event.h"
#include "core/frame/FrameView.h"
#include "core/frame/LocalFrame.h"
#include "core/frame/Settings.h"
#include "core/layout/LayoutView.h"
#include "core/layout/api/LayoutViewItem.h"
#include "core/page/FocusController.h"
#include "core/page/Page.h"
#include "platform/RuntimeEnabledFeatures.h"
#include "platform/wtf/AutoReset.h"
namespace blink {
SelectionController* SelectionController::Create(LocalFrame& frame) {
return new SelectionController(frame);
}
SelectionController::SelectionController(LocalFrame& frame)
: frame_(&frame),
mouse_down_may_start_select_(false),
mouse_down_was_single_click_in_selection_(false),
mouse_down_allows_multi_click_(false),
selection_state_(SelectionState::kHaveNotStartedSelection) {}
DEFINE_TRACE(SelectionController) {
visitor->Trace(frame_);
visitor->Trace(original_base_in_flat_tree_);
SynchronousMutationObserver::Trace(visitor);
}
namespace {
DispatchEventResult DispatchSelectStart(Node* node) {
if (!node || !node->GetLayoutObject())
return DispatchEventResult::kNotCanceled;
return node->DispatchEvent(
Event::CreateCancelableBubble(EventTypeNames::selectstart));
}
VisibleSelectionInFlatTree ExpandSelectionToRespectUserSelectAll(
Node* target_node,
const VisibleSelectionInFlatTree& selection) {
Node* const root_user_select_all =
EditingInFlatTreeStrategy::RootUserSelectAllForNode(target_node);
if (!root_user_select_all)
return selection;
return CreateVisibleSelection(
SelectionInFlatTree::Builder(selection.AsSelection())
.Collapse(MostBackwardCaretPosition(
PositionInFlatTree::BeforeNode(root_user_select_all),
kCanCrossEditingBoundary))
.Extend(MostForwardCaretPosition(
PositionInFlatTree::AfterNode(root_user_select_all),
kCanCrossEditingBoundary))
.Build());
}
static int TextDistance(const PositionInFlatTree& start,
const PositionInFlatTree& end) {
return TextIteratorInFlatTree::RangeLength(
start, end,
TextIteratorBehavior::AllVisiblePositionsRangeLengthBehavior());
}
bool CanMouseDownStartSelect(Node* node) {
if (!node || !node->GetLayoutObject())
return true;
if (!node->CanStartSelection())
return false;
return true;
}
VisiblePositionInFlatTree VisiblePositionOfHitTestResult(
const HitTestResult& hit_test_result) {
return CreateVisiblePosition(FromPositionInDOMTree<EditingInFlatTreeStrategy>(
hit_test_result.InnerNode()->GetLayoutObject()->PositionForPoint(
hit_test_result.LocalPoint())));
}
} // namespace
SelectionController::~SelectionController() = default;
Document& SelectionController::GetDocument() const {
DCHECK(frame_->GetDocument());
return *frame_->GetDocument();
}
void SelectionController::ContextDestroyed(Document*) {
original_base_in_flat_tree_ = VisiblePositionInFlatTree();
}
static PositionInFlatTree AdjustPositionRespectUserSelectAll(
Node* inner_node,
const PositionInFlatTree& selection_start,
const PositionInFlatTree& selection_end,
const PositionInFlatTree& position) {
const VisibleSelectionInFlatTree& selection_in_user_select_all =
ExpandSelectionToRespectUserSelectAll(
inner_node,
position.IsNull()
? VisibleSelectionInFlatTree()
: CreateVisibleSelection(
SelectionInFlatTree::Builder().Collapse(position).Build()));
if (!selection_in_user_select_all.IsRange())
return position;
if (selection_in_user_select_all.Start().CompareTo(selection_start) < 0)
return selection_in_user_select_all.Start();
if (selection_end.CompareTo(selection_in_user_select_all.end()) < 0)
return selection_in_user_select_all.end();
return position;
}
// Updating the selection is considered side-effect of the event and so it
// doesn't impact the handled state.
bool SelectionController::HandleSingleClick(
const MouseEventWithHitTestResults& event) {
TRACE_EVENT0("blink",
"SelectionController::handleMousePressEventSingleClick");
DCHECK(!frame_->GetDocument()->NeedsLayoutTreeUpdate());
Node* inner_node = event.InnerNode();
if (!(inner_node && inner_node->GetLayoutObject() &&
mouse_down_may_start_select_))
return false;
// Extend the selection if the Shift key is down, unless the click is in a
// link or image.
bool extend_selection = IsExtendingSelection(event);
const VisiblePositionInFlatTree& visible_hit_pos =
VisiblePositionOfHitTestResult(event.GetHitTestResult());
const VisiblePositionInFlatTree& visible_pos =
visible_hit_pos.IsNull()
? CreateVisiblePosition(
PositionInFlatTree::FirstPositionInOrBeforeNode(inner_node))
: visible_hit_pos;
const VisibleSelectionInFlatTree& selection =
this->Selection().ComputeVisibleSelectionInFlatTree();
// Don't restart the selection when the mouse is pressed on an
// existing selection so we can allow for text dragging.
if (FrameView* view = frame_->View()) {
const LayoutPoint v_point = view->RootFrameToContents(
FlooredIntPoint(event.Event().PositionInRootFrame()));
if (!extend_selection && this->Selection().Contains(v_point)) {
mouse_down_was_single_click_in_selection_ = true;
if (!event.Event().FromTouch())
return false;
if (!this->Selection().IsHandleVisible()) {
UpdateSelectionForMouseDownDispatchingSelectStart(
inner_node, selection, kCharacterGranularity,
HandleVisibility::kVisible);
return false;
}
}
}
if (extend_selection && !selection.IsNone()) {
// Note: "fast/events/shift-click-user-select-none.html" makes
// |pos.isNull()| true.
const PositionInFlatTree& pos = AdjustPositionRespectUserSelectAll(
inner_node, selection.Start(), selection.end(),
visible_pos.DeepEquivalent());
SelectionInFlatTree::Builder builder;
builder.SetGranularity(this->Selection().Granularity());
if (frame_->GetEditor().Behavior().ShouldConsiderSelectionAsDirectional()) {
builder.SetBaseAndExtent(selection.Base(), pos);
} else if (pos.IsNull()) {
builder.SetBaseAndExtent(selection.Base(), selection.Extent());
} else {
// Shift+Click deselects when selection was created right-to-left
const PositionInFlatTree& start = selection.Start();
const PositionInFlatTree& end = selection.end();
const int distance_to_start = TextDistance(start, pos);
const int distance_to_end = TextDistance(pos, end);
builder.SetBaseAndExtent(
distance_to_start <= distance_to_end ? end : start, pos);
}
UpdateSelectionForMouseDownDispatchingSelectStart(
inner_node, CreateVisibleSelection(builder.Build()),
this->Selection().Granularity(), HandleVisibility::kNotVisible);
return false;
}
if (selection_state_ == SelectionState::kExtendedSelection) {
UpdateSelectionForMouseDownDispatchingSelectStart(
inner_node, selection, kCharacterGranularity,
HandleVisibility::kNotVisible);
return false;
}
if (visible_pos.IsNull()) {
UpdateSelectionForMouseDownDispatchingSelectStart(
inner_node, VisibleSelectionInFlatTree(), kCharacterGranularity,
HandleVisibility::kNotVisible);
return false;
}
bool is_handle_visible = false;
if (HasEditableStyle(*inner_node)) {
const bool is_text_box_empty =
CreateVisibleSelection(SelectionInFlatTree::Builder()
.SelectAllChildren(*inner_node)
.Build())
.IsCaret();
const bool not_left_click =
event.Event().button != WebPointerProperties::Button::kLeft;
if (!is_text_box_empty || not_left_click)
is_handle_visible = event.Event().FromTouch();
}
UpdateSelectionForMouseDownDispatchingSelectStart(
inner_node,
ExpandSelectionToRespectUserSelectAll(
inner_node, CreateVisibleSelection(
SelectionInFlatTree::Builder()
.Collapse(visible_pos.ToPositionWithAffinity())
.Build())),
kCharacterGranularity,
is_handle_visible ? HandleVisibility::kVisible
: HandleVisibility::kNotVisible);
return false;
}
static bool TargetPositionIsBeforeDragStartPosition(
Node* drag_start_node,
const LayoutPoint& drag_start_point,
Node* target,
const LayoutPoint& hit_test_point) {
const PositionInFlatTree& target_position =
ToPositionInFlatTree(target->GetLayoutObject()
->PositionForPoint(hit_test_point)
.GetPosition());
const PositionInFlatTree& drag_start_position =
ToPositionInFlatTree(drag_start_node->GetLayoutObject()
->PositionForPoint(drag_start_point)
.GetPosition());
return target_position.CompareTo(drag_start_position) < 0;
}
static SelectionInFlatTree ApplySelectAll(
const PositionInFlatTree& base_position,
const PositionInFlatTree& target_position,
Node* mouse_press_node,
const LayoutPoint& drag_start_point,
Node* target,
const LayoutPoint& hit_test_point) {
Node* const root_user_select_all_for_mouse_press_node =
EditingInFlatTreeStrategy::RootUserSelectAllForNode(mouse_press_node);
Node* const root_user_select_all_for_target =
EditingInFlatTreeStrategy::RootUserSelectAllForNode(target);
if (root_user_select_all_for_mouse_press_node &&
root_user_select_all_for_mouse_press_node ==
root_user_select_all_for_target) {
return SelectionInFlatTree::Builder()
.SetBaseAndExtent(PositionInFlatTree::BeforeNode(
root_user_select_all_for_mouse_press_node),
PositionInFlatTree::AfterNode(
root_user_select_all_for_mouse_press_node))
.Build();
}
SelectionInFlatTree::Builder builder;
// Reset base for user select all when base is inside user-select-all area
// and extent < base.
if (root_user_select_all_for_mouse_press_node &&
TargetPositionIsBeforeDragStartPosition(
mouse_press_node, drag_start_point, target, hit_test_point)) {
builder.Collapse(PositionInFlatTree::AfterNode(
root_user_select_all_for_mouse_press_node));
} else {
builder.Collapse(base_position);
}
if (root_user_select_all_for_target && mouse_press_node->GetLayoutObject()) {
if (TargetPositionIsBeforeDragStartPosition(
mouse_press_node, drag_start_point, target, hit_test_point)) {
builder.Extend(
PositionInFlatTree::BeforeNode(root_user_select_all_for_target));
return builder.Build();
}
builder.Extend(
PositionInFlatTree::AfterNode(root_user_select_all_for_target));
return builder.Build();
}
builder.Extend(target_position);
return builder.Build();
}
void SelectionController::UpdateSelectionForMouseDrag(
const HitTestResult& hit_test_result,
Node* mouse_press_node,
const LayoutPoint& drag_start_pos,
const IntPoint& last_known_mouse_position) {
if (!mouse_down_may_start_select_)
return;
Node* target = hit_test_result.InnerNode();
if (!target)
return;
// TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
frame_->GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
const PositionWithAffinity& raw_target_position =
PositionRespectingEditingBoundary(
Selection().ComputeVisibleSelectionInDOMTree().Start(),
hit_test_result.LocalPoint(), target);
VisiblePositionInFlatTree target_position = CreateVisiblePosition(
FromPositionInDOMTree<EditingInFlatTreeStrategy>(raw_target_position));
// Don't modify the selection if we're not on a node.
if (target_position.IsNull())
return;
// Restart the selection if this is the first mouse move. This work is usually
// done in handleMousePressEvent, but not if the mouse press was on an
// existing selection.
// Special case to limit selection to the containing block for SVG text.
// FIXME: Isn't there a better non-SVG-specific way to do this?
if (Node* selection_base_node =
Selection().ComputeVisibleSelectionInFlatTree().Base().AnchorNode()) {
if (LayoutObject* selection_base_layout_object =
selection_base_node->GetLayoutObject()) {
if (selection_base_layout_object->IsSVGText()) {
if (target->GetLayoutObject()->ContainingBlock() !=
selection_base_layout_object->ContainingBlock())
return;
}
}
}
if (selection_state_ == SelectionState::kHaveNotStartedSelection &&
DispatchSelectStart(target) != DispatchEventResult::kNotCanceled)
return;
// TODO(yosin) We should check |mousePressNode|, |targetPosition|, and
// |newSelection| are valid for |m_frame->document()|.
// |dispatchSelectStart()| can change them by "selectstart" event handler.
PositionInFlatTree base_position;
if (selection_state_ != SelectionState::kExtendedSelection) {
// Always extend selection here because it's caused by a mouse drag
selection_state_ = SelectionState::kExtendedSelection;
base_position = target_position.DeepEquivalent();
} else {
base_position = Selection().ComputeVisibleSelectionInFlatTree().Base();
}
if (base_position.IsNull())
return;
const SelectionInFlatTree& applied_selection = ApplySelectAll(
base_position, target_position.DeepEquivalent(), mouse_press_node,
drag_start_pos, target, hit_test_result.LocalPoint());
SelectionInFlatTree::Builder builder(applied_selection);
if (Selection().Granularity() != kCharacterGranularity)
builder.SetGranularity(Selection().Granularity());
SetNonDirectionalSelectionIfNeeded(builder.Build(), Selection().Granularity(),
kAdjustEndpointsAtBidiBoundary,
HandleVisibility::kNotVisible);
}
bool SelectionController::UpdateSelectionForMouseDownDispatchingSelectStart(
Node* target_node,
const VisibleSelectionInFlatTree& selection,
TextGranularity granularity,
HandleVisibility handle_visibility) {
if (target_node && target_node->GetLayoutObject() &&
!target_node->GetLayoutObject()->IsSelectable())
return false;
if (DispatchSelectStart(target_node) != DispatchEventResult::kNotCanceled)
return false;
// |dispatchSelectStart()| can change document hosted by |m_frame|.
if (!this->Selection().IsAvailable())
return false;
if (!selection.IsValidFor(this->Selection().GetDocument()))
return false;
if (selection.IsRange()) {
selection_state_ = SelectionState::kExtendedSelection;
} else {
granularity = kCharacterGranularity;
selection_state_ = SelectionState::kPlacedCaret;
}
SetNonDirectionalSelectionIfNeeded(selection.AsSelection(), granularity,
kDoNotAdjustEndpoints, handle_visibility);
return true;
}
bool SelectionController::SelectClosestWordFromHitTestResult(
const HitTestResult& result,
AppendTrailingWhitespace append_trailing_whitespace,
SelectInputEventType select_input_event_type) {
Node* inner_node = result.InnerNode();
VisibleSelectionInFlatTree new_selection;
if (!inner_node || !inner_node->GetLayoutObject() ||
!inner_node->GetLayoutObject()->IsSelectable())
return false;
// Special-case image local offset to always be zero, to avoid triggering
// LayoutReplaced::positionFromPoint's advancement of the position at the
// mid-point of the the image (which was intended for mouse-drag selection
// and isn't desirable for touch).
HitTestResult adjusted_hit_test_result = result;
if (select_input_event_type == SelectInputEventType::kTouch &&
result.GetImage())
adjusted_hit_test_result.SetNodeAndPosition(result.InnerNode(),
LayoutPoint(0, 0));
const VisiblePositionInFlatTree& pos =
VisiblePositionOfHitTestResult(adjusted_hit_test_result);
if (pos.IsNotNull()) {
new_selection =
CreateVisibleSelection(SelectionInFlatTree::Builder()
.Collapse(pos.ToPositionWithAffinity())
.SetGranularity(kWordGranularity)
.Build());
}
HandleVisibility visibility = HandleVisibility::kNotVisible;
if (select_input_event_type == SelectInputEventType::kTouch) {
// If node doesn't have text except space, tab or line break, do not
// select that 'empty' area.
EphemeralRangeInFlatTree range(new_selection.Start(), new_selection.end());
const String& str = PlainText(
range,
TextIteratorBehavior::Builder()
.SetEmitsObjectReplacementCharacter(HasEditableStyle(*inner_node))
.Build());
if (str.IsEmpty() || str.SimplifyWhiteSpace().ContainsOnlyWhitespace())
return false;
if (new_selection.RootEditableElement() &&
pos.DeepEquivalent() == VisiblePositionInFlatTree::LastPositionInNode(
new_selection.RootEditableElement())
.DeepEquivalent())
return false;
visibility = HandleVisibility::kVisible;
}
if (append_trailing_whitespace == AppendTrailingWhitespace::kShouldAppend)
new_selection.AppendTrailingWhitespace();
return UpdateSelectionForMouseDownDispatchingSelectStart(
inner_node,
ExpandSelectionToRespectUserSelectAll(inner_node, new_selection),
kWordGranularity, visibility);
}
void SelectionController::SelectClosestMisspellingFromHitTestResult(
const HitTestResult& result,
AppendTrailingWhitespace append_trailing_whitespace) {
Node* inner_node = result.InnerNode();
VisibleSelectionInFlatTree new_selection;
if (!inner_node || !inner_node->GetLayoutObject())
return;
const VisiblePositionInFlatTree& pos = VisiblePositionOfHitTestResult(result);
if (pos.IsNotNull()) {
const PositionInFlatTree& marker_position =
pos.DeepEquivalent().ParentAnchoredEquivalent();
const DocumentMarker* const marker =
inner_node->GetDocument().Markers().MarkerAtPosition(
ToPositionInDOMTree(marker_position),
DocumentMarker::MisspellingMarkers());
if (marker) {
Node* container_node = marker_position.ComputeContainerNode();
const PositionInFlatTree start(container_node, marker->StartOffset());
const PositionInFlatTree end(container_node, marker->EndOffset());
new_selection = CreateVisibleSelection(
SelectionInFlatTree::Builder().Collapse(start).Extend(end).Build());
}
}
if (append_trailing_whitespace == AppendTrailingWhitespace::kShouldAppend)
new_selection.AppendTrailingWhitespace();
UpdateSelectionForMouseDownDispatchingSelectStart(
inner_node,
ExpandSelectionToRespectUserSelectAll(inner_node, new_selection),
kWordGranularity, HandleVisibility::kNotVisible);
}
void SelectionController::SelectClosestWordFromMouseEvent(
const MouseEventWithHitTestResults& result) {
if (!mouse_down_may_start_select_)
return;
AppendTrailingWhitespace append_trailing_whitespace =
(result.Event().click_count == 2 &&
frame_->GetEditor().IsSelectTrailingWhitespaceEnabled())
? AppendTrailingWhitespace::kShouldAppend
: AppendTrailingWhitespace::kDontAppend;
DCHECK(!frame_->GetDocument()->NeedsLayoutTreeUpdate());
SelectClosestWordFromHitTestResult(
result.GetHitTestResult(), append_trailing_whitespace,
result.Event().FromTouch() ? SelectInputEventType::kTouch
: SelectInputEventType::kMouse);
}
void SelectionController::SelectClosestMisspellingFromMouseEvent(
const MouseEventWithHitTestResults& result) {
if (!mouse_down_may_start_select_)
return;
SelectClosestMisspellingFromHitTestResult(
result.GetHitTestResult(),
(result.Event().click_count == 2 &&
frame_->GetEditor().IsSelectTrailingWhitespaceEnabled())
? AppendTrailingWhitespace::kShouldAppend
: AppendTrailingWhitespace::kDontAppend);
}
void SelectionController::SelectClosestWordOrLinkFromMouseEvent(
const MouseEventWithHitTestResults& result) {
if (!result.GetHitTestResult().IsLiveLink())
return SelectClosestWordFromMouseEvent(result);
Node* inner_node = result.InnerNode();
if (!inner_node || !inner_node->GetLayoutObject() ||
!mouse_down_may_start_select_)
return;
VisibleSelectionInFlatTree new_selection;
Element* url_element = result.GetHitTestResult().URLElement();
const VisiblePositionInFlatTree pos =
VisiblePositionOfHitTestResult(result.GetHitTestResult());
if (pos.IsNotNull() &&
pos.DeepEquivalent().AnchorNode()->IsDescendantOf(url_element)) {
new_selection = CreateVisibleSelection(
SelectionInFlatTree::Builder().SelectAllChildren(*url_element).Build());
}
UpdateSelectionForMouseDownDispatchingSelectStart(
inner_node,
ExpandSelectionToRespectUserSelectAll(inner_node, new_selection),
kWordGranularity, HandleVisibility::kNotVisible);
}
// TODO(xiaochengh): We should not use reference to return value.
static void AdjustEndpointsAtBidiBoundary(
VisiblePositionInFlatTree& visible_base,
VisiblePositionInFlatTree& visible_extent) {
DCHECK(visible_base.IsValid());
DCHECK(visible_extent.IsValid());
RenderedPosition base(visible_base);
RenderedPosition extent(visible_extent);
if (base.IsNull() || extent.IsNull() || base.IsEquivalent(extent))
return;
if (base.AtLeftBoundaryOfBidiRun()) {
if (!extent.AtRightBoundaryOfBidiRun(base.BidiLevelOnRight()) &&
base.IsEquivalent(
extent.LeftBoundaryOfBidiRun(base.BidiLevelOnRight()))) {
visible_base = CreateVisiblePosition(
ToPositionInFlatTree(base.PositionAtLeftBoundaryOfBiDiRun()));
return;
}
return;
}
if (base.AtRightBoundaryOfBidiRun()) {
if (!extent.AtLeftBoundaryOfBidiRun(base.BidiLevelOnLeft()) &&
base.IsEquivalent(
extent.RightBoundaryOfBidiRun(base.BidiLevelOnLeft()))) {
visible_base = CreateVisiblePosition(
ToPositionInFlatTree(base.PositionAtRightBoundaryOfBiDiRun()));
return;
}
return;
}
if (extent.AtLeftBoundaryOfBidiRun() &&
extent.IsEquivalent(
base.LeftBoundaryOfBidiRun(extent.BidiLevelOnRight()))) {
visible_extent = CreateVisiblePosition(
ToPositionInFlatTree(extent.PositionAtLeftBoundaryOfBiDiRun()));
return;
}
if (extent.AtRightBoundaryOfBidiRun() &&
extent.IsEquivalent(
base.RightBoundaryOfBidiRun(extent.BidiLevelOnLeft()))) {
visible_extent = CreateVisiblePosition(
ToPositionInFlatTree(extent.PositionAtRightBoundaryOfBiDiRun()));
return;
}
}
// TODO(yosin): We should take |granularity| and |handleVisibility| from
// |newSelection|.
void SelectionController::SetNonDirectionalSelectionIfNeeded(
const SelectionInFlatTree& passed_selection,
TextGranularity granularity,
EndPointsAdjustmentMode endpoints_adjustment_mode,
HandleVisibility handle_visibility) {
// TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
const VisibleSelectionInFlatTree& new_selection =
CreateVisibleSelection(passed_selection);
const PositionInFlatTree& base_position =
original_base_in_flat_tree_.DeepEquivalent();
const VisiblePositionInFlatTree& original_base =
base_position.IsConnected() ? CreateVisiblePosition(base_position)
: VisiblePositionInFlatTree();
const VisiblePositionInFlatTree& base =
original_base.IsNotNull() ? original_base
: CreateVisiblePosition(new_selection.Base());
VisiblePositionInFlatTree new_base = base;
const VisiblePositionInFlatTree& extent =
CreateVisiblePosition(new_selection.Extent());
VisiblePositionInFlatTree new_extent = extent;
if (endpoints_adjustment_mode == kAdjustEndpointsAtBidiBoundary)
AdjustEndpointsAtBidiBoundary(new_base, new_extent);
SelectionInFlatTree::Builder builder(new_selection.AsSelection());
if (new_base.DeepEquivalent() != base.DeepEquivalent() ||
new_extent.DeepEquivalent() != extent.DeepEquivalent()) {
original_base_in_flat_tree_ = base;
SetContext(&GetDocument());
builder.SetBaseAndExtent(new_base.DeepEquivalent(),
new_extent.DeepEquivalent());
} else if (original_base.IsNotNull()) {
if (Selection().ComputeVisibleSelectionInFlatTree().Base() ==
new_selection.Base()) {
builder.SetBaseAndExtent(original_base.DeepEquivalent(),
new_selection.Extent());
}
original_base_in_flat_tree_ = VisiblePositionInFlatTree();
}
builder.SetIsHandleVisible(handle_visibility == HandleVisibility::kVisible)
.SetIsDirectional(frame_->GetEditor()
.Behavior()
.ShouldConsiderSelectionAsDirectional() ||
new_selection.IsDirectional());
const SelectionInFlatTree& selection_in_flat_tree = builder.Build();
if (Selection().ComputeVisibleSelectionInFlatTree() ==
CreateVisibleSelection(selection_in_flat_tree) &&
Selection().IsHandleVisible() == selection_in_flat_tree.IsHandleVisible())
return;
Selection().SetSelection(
selection_in_flat_tree,
FrameSelection::kCloseTyping | FrameSelection::kClearTypingStyle,
CursorAlignOnScroll::kIfNeeded, granularity);
}
void SelectionController::SetCaretAtHitTestResult(
const HitTestResult& hit_test_result) {
Node* inner_node = hit_test_result.InnerNode();
const VisiblePositionInFlatTree& visible_hit_pos =
VisiblePositionOfHitTestResult(hit_test_result);
const VisiblePositionInFlatTree& visible_pos =
visible_hit_pos.IsNull()
? CreateVisiblePosition(
PositionInFlatTree::FirstPositionInOrBeforeNode(inner_node))
: visible_hit_pos;
if (visible_pos.IsNull()) {
UpdateSelectionForMouseDownDispatchingSelectStart(
inner_node, VisibleSelectionInFlatTree(), kCharacterGranularity,
HandleVisibility::kVisible);
return;
}
UpdateSelectionForMouseDownDispatchingSelectStart(
inner_node,
ExpandSelectionToRespectUserSelectAll(
inner_node, CreateVisibleSelection(
SelectionInFlatTree::Builder()
.Collapse(visible_pos.ToPositionWithAffinity())
.Build())),
kCharacterGranularity, HandleVisibility::kVisible);
}
bool SelectionController::HandleDoubleClick(
const MouseEventWithHitTestResults& event) {
TRACE_EVENT0("blink",
"SelectionController::handleMousePressEventDoubleClick");
if (!Selection().IsAvailable())
return false;
if (!mouse_down_allows_multi_click_)
return HandleSingleClick(event);
if (event.Event().button != WebPointerProperties::Button::kLeft)
return false;
if (Selection().ComputeVisibleSelectionInDOMTreeDeprecated().IsRange()) {
// A double-click when range is already selected
// should not change the selection. So, do not call
// selectClosestWordFromMouseEvent, but do set
// m_beganSelectingText to prevent handleMouseReleaseEvent
// from setting caret selection.
selection_state_ = SelectionState::kExtendedSelection;
} else {
SelectClosestWordFromMouseEvent(event);
}
return true;
}
bool SelectionController::HandleTripleClick(
const MouseEventWithHitTestResults& event) {
TRACE_EVENT0("blink",
"SelectionController::handleMousePressEventTripleClick");
if (!Selection().IsAvailable()) {
// editing/shadow/doubleclick-on-meter-in-shadow-crash.html reach here.
return false;
}
if (!mouse_down_allows_multi_click_)
return HandleSingleClick(event);
if (event.Event().button != WebPointerProperties::Button::kLeft)
return false;
Node* inner_node = event.InnerNode();
if (!(inner_node && inner_node->GetLayoutObject() &&
mouse_down_may_start_select_))
return false;
VisibleSelectionInFlatTree new_selection;
const VisiblePositionInFlatTree& pos =
VisiblePositionOfHitTestResult(event.GetHitTestResult());
if (pos.IsNotNull()) {
new_selection =
CreateVisibleSelection(SelectionInFlatTree::Builder()
.Collapse(pos.ToPositionWithAffinity())
.SetGranularity(kParagraphGranularity)
.Build());
}
const bool is_handle_visible =
event.Event().FromTouch() && new_selection.IsRange();
return UpdateSelectionForMouseDownDispatchingSelectStart(
inner_node,
ExpandSelectionToRespectUserSelectAll(inner_node, new_selection),
kParagraphGranularity,
is_handle_visible ? HandleVisibility::kVisible
: HandleVisibility::kNotVisible);
}
bool SelectionController::HandleMousePressEvent(
const MouseEventWithHitTestResults& event) {
TRACE_EVENT0("blink", "SelectionController::handleMousePressEvent");
// If we got the event back, that must mean it wasn't prevented,
// so it's allowed to start a drag or selection if it wasn't in a scrollbar.
mouse_down_may_start_select_ =
(CanMouseDownStartSelect(event.InnerNode()) || IsLinkSelection(event)) &&
!event.GetScrollbar();
mouse_down_was_single_click_in_selection_ = false;
if (!Selection().IsAvailable()) {
// "gesture-tap-frame-removed.html" reaches here.
mouse_down_allows_multi_click_ = !event.Event().FromTouch();
} else {
// Avoid double-tap touch gesture confusion by restricting multi-click side
// effects, e.g., word selection, to editable regions.
mouse_down_allows_multi_click_ =
!event.Event().FromTouch() ||
Selection()
.ComputeVisibleSelectionInDOMTreeDeprecated()
.HasEditableStyle();
}
if (event.Event().click_count >= 3)
return HandleTripleClick(event);
if (event.Event().click_count == 2)
return HandleDoubleClick(event);
return HandleSingleClick(event);
}
void SelectionController::HandleMouseDraggedEvent(
const MouseEventWithHitTestResults& event,
const IntPoint& mouse_down_pos,
const LayoutPoint& drag_start_pos,
Node* mouse_press_node,
const IntPoint& last_known_mouse_position) {
TRACE_EVENT0("blink", "SelectionController::handleMouseDraggedEvent");
if (!Selection().IsAvailable())
return;
if (selection_state_ != SelectionState::kExtendedSelection) {
HitTestRequest request(HitTestRequest::kReadOnly | HitTestRequest::kActive);
HitTestResult result(request, mouse_down_pos);
frame_->GetDocument()->GetLayoutViewItem().HitTest(result);
UpdateSelectionForMouseDrag(result, mouse_press_node, drag_start_pos,
last_known_mouse_position);
}
UpdateSelectionForMouseDrag(event.GetHitTestResult(), mouse_press_node,
drag_start_pos, last_known_mouse_position);
}
void SelectionController::UpdateSelectionForMouseDrag(
Node* mouse_press_node,
const LayoutPoint& drag_start_pos,
const IntPoint& last_known_mouse_position) {
FrameView* view = frame_->View();
if (!view)
return;
LayoutViewItem layout_item = frame_->ContentLayoutItem();
if (layout_item.IsNull())
return;
HitTestRequest request(HitTestRequest::kReadOnly | HitTestRequest::kActive |
HitTestRequest::kMove);
HitTestResult result(request,
view->RootFrameToContents(last_known_mouse_position));
layout_item.HitTest(result);
UpdateSelectionForMouseDrag(result, mouse_press_node, drag_start_pos,
last_known_mouse_position);
}
bool SelectionController::HandleMouseReleaseEvent(
const MouseEventWithHitTestResults& event,
const LayoutPoint& drag_start_pos) {
TRACE_EVENT0("blink", "SelectionController::handleMouseReleaseEvent");
if (!Selection().IsAvailable())
return false;
bool handled = false;
mouse_down_may_start_select_ = false;
// Clear the selection if the mouse didn't move after the last mouse
// press and it's not a context menu click. We do this so when clicking
// on the selection, the selection goes away. However, if we are
// editing, place the caret.
if (mouse_down_was_single_click_in_selection_ &&
selection_state_ != SelectionState::kExtendedSelection &&
drag_start_pos == FlooredIntPoint(event.Event().PositionInRootFrame()) &&
Selection().ComputeVisibleSelectionInDOMTreeDeprecated().IsRange() &&
event.Event().button != WebPointerProperties::Button::kRight) {
// TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
frame_->GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
SelectionInFlatTree::Builder builder;
Node* node = event.InnerNode();
if (node && node->GetLayoutObject() && HasEditableStyle(*node)) {
const VisiblePositionInFlatTree pos =
VisiblePositionOfHitTestResult(event.GetHitTestResult());
if (pos.IsNotNull())
builder.Collapse(pos.ToPositionWithAffinity());
}
if (Selection().ComputeVisibleSelectionInFlatTree() !=
CreateVisibleSelection(builder.Build())) {
Selection().SetSelection(builder.Build());
}
handled = true;
}
Selection().NotifyTextControlOfSelectionChange(kUserTriggered);
Selection().SelectFrameElementInParentIfFullySelected();
if (event.Event().button == WebPointerProperties::Button::kMiddle &&
!event.IsOverLink()) {
// Ignore handled, since we want to paste to where the caret was placed
// anyway.
handled = HandlePasteGlobalSelection(event.Event()) || handled;
}
return handled;
}
bool SelectionController::HandlePasteGlobalSelection(
const WebMouseEvent& mouse_event) {
// If the event was a middle click, attempt to copy global selection in after
// the newly set caret position.
//
// This code is called from either the mouse up or mouse down handling. There
// is some debate about when the global selection is pasted:
// xterm: pastes on up.
// GTK: pastes on down.
// Qt: pastes on up.
// Firefox: pastes on up.
// Chromium: pastes on up.
//
// There is something of a webcompat angle to this well, as highlighted by
// crbug.com/14608. Pages can clear text boxes 'onclick' and, if we paste on
// down then the text is pasted just before the onclick handler runs and
// clears the text box. So it's important this happens after the event
// handlers have been fired.
if (mouse_event.GetType() != WebInputEvent::kMouseUp)
return false;
if (!frame_->GetPage())
return false;
Frame* focus_frame =
frame_->GetPage()->GetFocusController().FocusedOrMainFrame();
// Do not paste here if the focus was moved somewhere else.
if (frame_ == focus_frame &&
frame_->GetEditor().Behavior().SupportsGlobalSelection())
return frame_->GetEditor().CreateCommand("PasteGlobalSelection").Execute();
return false;
}
bool SelectionController::HandleGestureLongPress(
const HitTestResult& hit_test_result) {
TRACE_EVENT0("blink", "SelectionController::handleGestureLongPress");
if (!Selection().IsAvailable())
return false;
if (hit_test_result.IsLiveLink())
return false;
Node* inner_node = hit_test_result.InnerNode();
inner_node->GetDocument().UpdateStyleAndLayoutTree();
bool inner_node_is_selectable = HasEditableStyle(*inner_node) ||
inner_node->IsTextNode() ||
inner_node->CanStartSelection();
if (!inner_node_is_selectable)
return false;
if (SelectClosestWordFromHitTestResult(hit_test_result,
AppendTrailingWhitespace::kDontAppend,
SelectInputEventType::kTouch))
return Selection().IsAvailable();
if (!inner_node->isConnected() || !inner_node->GetLayoutObject())
return false;
SetCaretAtHitTestResult(hit_test_result);
return false;
}
void SelectionController::HandleGestureTwoFingerTap(
const GestureEventWithHitTestResults& targeted_event) {
TRACE_EVENT0("blink", "SelectionController::handleGestureTwoFingerTap");
SetCaretAtHitTestResult(targeted_event.GetHitTestResult());
}
void SelectionController::HandleGestureLongTap(
const GestureEventWithHitTestResults& targeted_event) {
TRACE_EVENT0("blink", "SelectionController::handleGestureLongTap");
SetCaretAtHitTestResult(targeted_event.GetHitTestResult());
}
static bool HitTestResultIsMisspelled(const HitTestResult& result) {
Node* inner_node = result.InnerNode();
if (!inner_node || !inner_node->GetLayoutObject())
return false;
VisiblePosition pos = CreateVisiblePosition(
inner_node->GetLayoutObject()->PositionForPoint(result.LocalPoint()));
if (pos.IsNull())
return false;
const Position& marker_position =
pos.DeepEquivalent().ParentAnchoredEquivalent();
return inner_node->GetDocument().Markers().MarkerAtPosition(
marker_position, DocumentMarker::MisspellingMarkers());
}
void SelectionController::SendContextMenuEvent(
const MouseEventWithHitTestResults& mev,
const LayoutPoint& position) {
if (!Selection().IsAvailable())
return;
if (Selection().Contains(position) || mev.GetScrollbar() ||
// FIXME: In the editable case, word selection sometimes selects content
// that isn't underneath the mouse.
// If the selection is non-editable, we do word selection to make it
// easier to use the contextual menu items available for text selections.
// But only if we're above text.
!(Selection()
.ComputeVisibleSelectionInDOMTreeDeprecated()
.IsContentEditable() ||
(mev.InnerNode() && mev.InnerNode()->IsTextNode())))
return;
// Context menu events are always allowed to perform a selection.
AutoReset<bool> mouse_down_may_start_select_change(
&mouse_down_may_start_select_, true);
if (HitTestResultIsMisspelled(mev.GetHitTestResult()))
return SelectClosestMisspellingFromMouseEvent(mev);
if (!frame_->GetEditor().Behavior().ShouldSelectOnContextualMenuClick())
return;
SelectClosestWordOrLinkFromMouseEvent(mev);
}
void SelectionController::PassMousePressEventToSubframe(
const MouseEventWithHitTestResults& mev) {
// TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
frame_->GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
// If we're clicking into a frame that is selected, the frame will appear
// greyed out even though we're clicking on the selection. This looks
// really strange (having the whole frame be greyed out), so we deselect the
// selection.
IntPoint p = frame_->View()->RootFrameToContents(
FlooredIntPoint(mev.Event().PositionInRootFrame()));
if (!Selection().Contains(p))
return;
const VisiblePositionInFlatTree& visible_pos =
VisiblePositionOfHitTestResult(mev.GetHitTestResult());
if (visible_pos.IsNull()) {
Selection().SetSelection(SelectionInFlatTree());
return;
}
Selection().SetSelection(SelectionInFlatTree::Builder()
.Collapse(visible_pos.ToPositionWithAffinity())
.Build());
}
void SelectionController::InitializeSelectionState() {
selection_state_ = SelectionState::kHaveNotStartedSelection;
}
void SelectionController::SetMouseDownMayStartSelect(bool may_start_select) {
mouse_down_may_start_select_ = may_start_select;
}
bool SelectionController::MouseDownMayStartSelect() const {
return mouse_down_may_start_select_;
}
bool SelectionController::MouseDownWasSingleClickInSelection() const {
return mouse_down_was_single_click_in_selection_;
}
void SelectionController::NotifySelectionChanged() {
// To avoid regression on speedometer benchmark[1] test, we should not
// update layout tree in this code block.
// [1] http://browserbench.org/Speedometer/
DocumentLifecycle::DisallowTransitionScope disallow_transition(
frame_->GetDocument()->Lifecycle());
const SelectionInDOMTree& selection =
this->Selection().GetSelectionInDOMTree();
switch (selection.SelectionTypeWithLegacyGranularity()) {
case kNoSelection:
selection_state_ = SelectionState::kHaveNotStartedSelection;
return;
case kCaretSelection:
selection_state_ = SelectionState::kPlacedCaret;
return;
case kRangeSelection:
selection_state_ = SelectionState::kExtendedSelection;
return;
}
NOTREACHED() << "We should handle all SelectionType" << selection;
}
FrameSelection& SelectionController::Selection() const {
return frame_->Selection();
}
bool IsLinkSelection(const MouseEventWithHitTestResults& event) {
return (event.Event().GetModifiers() & WebInputEvent::Modifiers::kAltKey) !=
0 &&
event.IsOverLink();
}
bool IsExtendingSelection(const MouseEventWithHitTestResults& event) {
bool is_mouse_down_on_link_or_image =
event.IsOverLink() || event.GetHitTestResult().GetImage();
return (event.Event().GetModifiers() & WebInputEvent::Modifiers::kShiftKey) !=
0 &&
!is_mouse_down_on_link_or_image;
}
} // namespace blink