// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ui/touch_selection/touch_selection_controller.h" #include #include "base/auto_reset.h" #include "base/check_op.h" #include "base/metrics/histogram_macros.h" #include "base/metrics/user_metrics.h" #include "base/notreached.h" namespace ui { namespace { gfx::Vector2dF ComputeLineOffsetFromBottom(const gfx::SelectionBound& bound) { gfx::Vector2dF line_offset = gfx::ScaleVector2d(bound.edge_start() - bound.edge_end(), 0.5f); // An offset of 8 DIPs is sufficient for most line sizes. For small lines, // using half the line height avoids synthesizing a point on a line above // (or below) the intended line. const gfx::Vector2dF kMaxLineOffset(8.f, 8.f); line_offset.SetToMin(kMaxLineOffset); line_offset.SetToMax(-kMaxLineOffset); return line_offset; } TouchHandleOrientation ToTouchHandleOrientation( gfx::SelectionBound::Type type) { switch (type) { case gfx::SelectionBound::LEFT: return TouchHandleOrientation::LEFT; case gfx::SelectionBound::RIGHT: return TouchHandleOrientation::RIGHT; case gfx::SelectionBound::CENTER: return TouchHandleOrientation::CENTER; case gfx::SelectionBound::EMPTY: return TouchHandleOrientation::UNDEFINED; } NOTREACHED() << "Invalid selection bound type: " << type; return TouchHandleOrientation::UNDEFINED; } } // namespace TouchSelectionController::Config::Config() : max_tap_duration(base::Milliseconds(300)), tap_slop(8), enable_adaptive_handle_orientation(false), enable_longpress_drag_selection(false), hide_active_handle(false) {} TouchSelectionController::Config::~Config() { } TouchSelectionController::TouchSelectionController( TouchSelectionControllerClient* client, const Config& config) : client_(client), config_(config), response_pending_input_event_(INPUT_EVENT_TYPE_NONE), start_orientation_(TouchHandleOrientation::UNDEFINED), end_orientation_(TouchHandleOrientation::UNDEFINED), active_status_(INACTIVE), temporarily_hidden_(false), anchor_drag_to_selection_start_(false), longpress_drag_selector_(this), selection_handle_dragged_(false), consume_touch_sequence_(false), show_touch_handles_(false) { DCHECK(client_); } TouchSelectionController::~TouchSelectionController() { } void TouchSelectionController::OnSelectionBoundsChanged( const gfx::SelectionBound& start, const gfx::SelectionBound& end) { if (start == start_ && end_ == end) return; if (start.type() == gfx::SelectionBound::EMPTY || end.type() == gfx::SelectionBound::EMPTY || !show_touch_handles_) { HideHandles(); return; } // Swap the Handles when the start and end selection points cross each other. if (active_status_ == SELECTION_ACTIVE) { // Bounds have the same orientation. bool need_swap = (start_selection_handle_->IsActive() && end_.edge_end() == start.edge_end()) || (end_selection_handle_->IsActive() && end.edge_end() == start_.edge_end()); // Bounds have different orientation. // Specifically, for writing-mode: vertical-*, selection bounds are // horizontal. // When vertical-lr: // - start bound is from right to left, // - end bound is from left to right. // When vertical-rl: // - start bound is from left to right, // - end bound is from right to left. // So when previous start/end bound become current end/start bound, // edge_start() and edge_end() are swapped. Therefore, we are comparing // edge_end() with edge_start() here. need_swap |= (start_selection_handle_->IsActive() && end_.edge_end() == start.edge_start()) || (end_selection_handle_->IsActive() && end.edge_end() == start_.edge_start()); if (need_swap) start_selection_handle_.swap(end_selection_handle_); } // Update |anchor_drag_to_selection_start_| for long press drag selector. // Since selection can be updated with only one end at a time, if one end is // equal to the previous value, the updated end is the other. if (start_ == start) anchor_drag_to_selection_start_ = false; else if (end_ == end) anchor_drag_to_selection_start_ = true; start_ = start; end_ = end; start_orientation_ = ToTouchHandleOrientation(start_.type()); end_orientation_ = ToTouchHandleOrientation(end_.type()); // Ensure that |response_pending_input_event_| is cleared after the method // completes, while also making its current value available for the duration // of the call. InputEventType causal_input_event = response_pending_input_event_; response_pending_input_event_ = INPUT_EVENT_TYPE_NONE; base::AutoReset auto_reset_response_pending_input_event( &response_pending_input_event_, causal_input_event); if ((start_orientation_ == TouchHandleOrientation::LEFT || start_orientation_ == TouchHandleOrientation::RIGHT) && (end_orientation_ == TouchHandleOrientation::RIGHT || end_orientation_ == TouchHandleOrientation::LEFT)) { OnSelectionChanged(); return; } if (start_orientation_ == TouchHandleOrientation::CENTER) { OnInsertionChanged(); return; } HideHandles(); } void TouchSelectionController::OnViewportChanged( const gfx::RectF viewport_rect) { // Trigger a force update if the viewport is changed, so that // it triggers a call to change the mirror values if required. if (viewport_rect_ == viewport_rect) return; viewport_rect_ = viewport_rect; if (active_status_ == INACTIVE) return; if (active_status_ == INSERTION_ACTIVE) { DCHECK(insertion_handle_); insertion_handle_->SetViewportRect(viewport_rect); } else if (active_status_ == SELECTION_ACTIVE) { DCHECK(start_selection_handle_); DCHECK(end_selection_handle_); start_selection_handle_->SetViewportRect(viewport_rect); end_selection_handle_->SetViewportRect(viewport_rect); } // Update handle layout after setting the new Viewport size. UpdateHandleLayoutIfNecessary(); } bool TouchSelectionController::WillHandleTouchEvent(const MotionEvent& event) { bool handled = WillHandleTouchEventImpl(event); // If Action::DOWN is consumed, the rest of touch sequence should be consumed, // too, regardless of value of |handled|. // TODO(mohsen): This will consume touch events until the next Action::DOWN. // Ideally we should consume until the final Action::UP/Action::CANCEL. // But, apparently, we can't reliably determine the final Action::CANCEL in a // multi-touch scenario. See https://crbug.com/653212. if (event.GetAction() == MotionEvent::Action::DOWN) consume_touch_sequence_ = handled; return handled || consume_touch_sequence_; } void TouchSelectionController::HandleTapEvent(const gfx::PointF& location, int tap_count) { if (tap_count > 1) { response_pending_input_event_ = REPEATED_TAP; } else { response_pending_input_event_ = TAP; } } void TouchSelectionController::HandleLongPressEvent( base::TimeTicks event_time, const gfx::PointF& location) { longpress_drag_selector_.OnLongPressEvent(event_time, location); response_pending_input_event_ = LONG_PRESS; } void TouchSelectionController::OnScrollBeginEvent() { // When there is an active selection, if the user performs a long-press that // does not trigger a new selection (e.g. a long-press on an empty area) and // then scrolls, the scroll will move the selection. In this case we will // think incorrectly that the selection change was due to the long-press and // will activate touch selection and start long-press drag gesture (see // ActivateInsertionIfNecessary()). To prevent this, we need to reset the // state of touch selection controller and long-press drag selector. // TODO(mohsen): Remove this workaround when we have enough information about // the cause of a selection change (see https://crbug.com/571897). longpress_drag_selector_.OnScrollBeginEvent(); response_pending_input_event_ = INPUT_EVENT_TYPE_NONE; } void TouchSelectionController::HideHandles() { response_pending_input_event_ = INPUT_EVENT_TYPE_NONE; DeactivateInsertion(); DeactivateSelection(); start_ = gfx::SelectionBound(); end_ = gfx::SelectionBound(); start_orientation_ = ToTouchHandleOrientation(start_.type()); end_orientation_ = ToTouchHandleOrientation(end_.type()); } void TouchSelectionController::HideAndDisallowShowingAutomatically() { HideHandles(); show_touch_handles_ = false; } void TouchSelectionController::SetTemporarilyHidden(bool hidden) { if (temporarily_hidden_ == hidden) return; temporarily_hidden_ = hidden; RefreshHandleVisibility(); } bool TouchSelectionController::Animate(base::TimeTicks frame_time) { if (active_status_ == INSERTION_ACTIVE) return insertion_handle_->Animate(frame_time); if (active_status_ == SELECTION_ACTIVE) { bool needs_animate = start_selection_handle_->Animate(frame_time); needs_animate |= end_selection_handle_->Animate(frame_time); return needs_animate; } return false; } gfx::RectF TouchSelectionController::GetRectBetweenBounds() const { // Short-circuit for efficiency. if (active_status_ == INACTIVE) return gfx::RectF(); if (start_.visible() && !end_.visible()) { // This BoundingRect is actually a line unless the selection is rotated. return gfx::BoundingRect(start_.edge_start(), start_.edge_end()); } if (end_.visible() && !start_.visible()) { // This BoundingRect is actually a line unless the selection is rotated. return gfx::BoundingRect(end_.edge_start(), end_.edge_end()); } // If both handles are visible, or both are invisible, use the entire rect. // Specifically, if both handles are on the same horizontal line for // writing-mode: vertical-*, or both are on the same vertical line for // writing-mode: horizontal, the entire rect is actually a line unless the // selection is rotated. return RectFBetweenSelectionBounds(start_, end_); } gfx::RectF TouchSelectionController::GetVisibleRectBetweenBounds() const { // Short-circuit for efficiency. if (active_status_ == INACTIVE) return gfx::RectF(); // Returns the rect of the entire visible selection rect. return RectFBetweenVisibleSelectionBounds(start_, end_); } gfx::RectF TouchSelectionController::GetStartHandleRect() const { if (active_status_ == INSERTION_ACTIVE) return insertion_handle_->GetVisibleBounds(); if (active_status_ == SELECTION_ACTIVE) return start_selection_handle_->GetVisibleBounds(); return gfx::RectF(); } gfx::RectF TouchSelectionController::GetEndHandleRect() const { if (active_status_ == INSERTION_ACTIVE) return insertion_handle_->GetVisibleBounds(); if (active_status_ == SELECTION_ACTIVE) return end_selection_handle_->GetVisibleBounds(); return gfx::RectF(); } float TouchSelectionController::GetTouchHandleHeight() const { if (active_status_ == INSERTION_ACTIVE) return insertion_handle_->GetVisibleBounds().height(); if (active_status_ == SELECTION_ACTIVE) { if (GetStartVisible()) return start_selection_handle_->GetVisibleBounds().height(); if (GetEndVisible()) return end_selection_handle_->GetVisibleBounds().height(); } return 0.f; } float TouchSelectionController::GetActiveHandleMiddleY() const { const gfx::SelectionBound* bound = nullptr; if (active_status_ == INSERTION_ACTIVE && insertion_handle_->IsActive()) bound = &start_; if (active_status_ == SELECTION_ACTIVE) { if (start_selection_handle_->IsActive()) bound = &start_; else if (end_selection_handle_->IsActive()) bound = &end_; } if (!bound) return 0.f; return (bound->edge_start().y() + bound->edge_end().y()) / 2.f; } const gfx::PointF& TouchSelectionController::GetStartPosition() const { return start_.edge_end(); } const gfx::PointF& TouchSelectionController::GetEndPosition() const { return end_.edge_end(); } bool TouchSelectionController::WillHandleTouchEventImpl( const MotionEvent& event) { show_touch_handles_ = true; if (config_.enable_longpress_drag_selection && longpress_drag_selector_.WillHandleTouchEvent(event)) { return true; } if (active_status_ == INSERTION_ACTIVE) { DCHECK(insertion_handle_); return insertion_handle_->WillHandleTouchEvent(event); } if (active_status_ == SELECTION_ACTIVE) { DCHECK(start_selection_handle_); DCHECK(end_selection_handle_); if (start_selection_handle_->IsActive()) return start_selection_handle_->WillHandleTouchEvent(event); if (end_selection_handle_->IsActive()) return end_selection_handle_->WillHandleTouchEvent(event); const gfx::PointF event_pos(event.GetX(), event.GetY()); if ((event_pos - GetStartPosition()).LengthSquared() <= (event_pos - GetEndPosition()).LengthSquared()) { return start_selection_handle_->WillHandleTouchEvent(event); } return end_selection_handle_->WillHandleTouchEvent(event); } return false; } void TouchSelectionController::OnSwipeToMoveCursorBegin() { if (config_.hide_active_handle) { // Hide the handle when magnifier is showing since it can confuse the user. SetTemporarilyHidden(true); // If the user has typed something, the insertion handle might be hidden. // Prepare to show touch handles on end. show_touch_handles_ = true; } } void TouchSelectionController::OnSwipeToMoveCursorEnd() { // Show the handle at the end if magnifier was showing. if (config_.hide_active_handle) SetTemporarilyHidden(false); } void TouchSelectionController::OnDragBegin( const TouchSelectionDraggable& draggable, const gfx::PointF& drag_position) { if (&draggable == insertion_handle_.get()) { DCHECK_EQ(active_status_, INSERTION_ACTIVE); if (config_.hide_active_handle) insertion_handle_->SetTransparent(); client_->OnSelectionEvent(INSERTION_HANDLE_DRAG_STARTED); anchor_drag_to_selection_start_ = true; return; } DCHECK_EQ(active_status_, SELECTION_ACTIVE); if (&draggable == start_selection_handle_.get()) { anchor_drag_to_selection_start_ = true; } else if (&draggable == end_selection_handle_.get()) { anchor_drag_to_selection_start_ = false; } else { DCHECK_EQ(&draggable, &longpress_drag_selector_); anchor_drag_to_selection_start_ = (drag_position - GetStartPosition()).LengthSquared() < (drag_position - GetEndPosition()).LengthSquared(); } if (config_.hide_active_handle) { if (&draggable == start_selection_handle_.get()) { start_selection_handle_->SetTransparent(); } else if (&draggable == end_selection_handle_.get()) { end_selection_handle_->SetTransparent(); } } gfx::PointF base = GetStartPosition() + GetStartLineOffset(); gfx::PointF extent = GetEndPosition() + GetEndLineOffset(); if (anchor_drag_to_selection_start_) std::swap(base, extent); // If this is the first drag, log an action to allow user action sequencing. if (!selection_handle_dragged_) base::RecordAction(base::UserMetricsAction("SelectionChanged")); selection_handle_dragged_ = true; // When moving the handle we want to move only the extent point. Before doing // so we must make sure that the base point is set correctly. client_->SelectBetweenCoordinates(base, extent); client_->OnSelectionEvent(SELECTION_HANDLE_DRAG_STARTED); } void TouchSelectionController::OnDragUpdate( const TouchSelectionDraggable& draggable, const gfx::PointF& drag_position) { // As the position corresponds to the bottom left point of the selection // bound, offset it to some reasonable point on the current line of text. gfx::Vector2dF line_offset = anchor_drag_to_selection_start_ ? GetStartLineOffset() : GetEndLineOffset(); gfx::PointF line_position = drag_position + line_offset; if (&draggable == insertion_handle_.get()) client_->MoveCaret(line_position); else client_->MoveRangeSelectionExtent(line_position); // We use the bound middle point to restrict the ability to move up and // down, but let user move it more freely in horizontal direction. if (&draggable == &longpress_drag_selector_) { // Show magnifier at the selection edge. const gfx::SelectionBound* bound = anchor_drag_to_selection_start_ ? &start_ : &end_; const float x = bound->edge_start().x(); const float y = (bound->edge_start().y() + bound->edge_end().y()) / 2.f; client_->OnDragUpdate(TouchSelectionDraggable::Type::kLongpress, gfx::PointF(x, y)); } else { const float y = GetActiveHandleMiddleY(); client_->OnDragUpdate(TouchSelectionDraggable::Type::kTouchHandle, gfx::PointF(drag_position.x(), y)); } } void TouchSelectionController::OnDragEnd( const TouchSelectionDraggable& draggable) { if (&draggable == insertion_handle_.get()) client_->OnSelectionEvent(INSERTION_HANDLE_DRAG_STOPPED); else client_->OnSelectionEvent(SELECTION_HANDLE_DRAG_STOPPED); } bool TouchSelectionController::IsWithinTapSlop( const gfx::Vector2dF& delta) const { return delta.LengthSquared() < (static_cast(config_.tap_slop) * config_.tap_slop); } void TouchSelectionController::OnHandleTapped(const TouchHandle& handle) { if (insertion_handle_ && &handle == insertion_handle_.get()) client_->OnSelectionEvent(INSERTION_HANDLE_TAPPED); } void TouchSelectionController::SetNeedsAnimate() { client_->SetNeedsAnimate(); } std::unique_ptr TouchSelectionController::CreateDrawable() { return client_->CreateDrawable(); } base::TimeDelta TouchSelectionController::GetMaxTapDuration() const { return config_.max_tap_duration; } bool TouchSelectionController::IsAdaptiveHandleOrientationEnabled() const { return config_.enable_adaptive_handle_orientation; } void TouchSelectionController::OnLongPressDragActiveStateChanged() { // The handles should remain hidden for the duration of a longpress drag, // including the time between a longpress and the start of drag motion. RefreshHandleVisibility(); } gfx::PointF TouchSelectionController::GetSelectionStart() const { return GetStartPosition(); } gfx::PointF TouchSelectionController::GetSelectionEnd() const { return GetEndPosition(); } void TouchSelectionController::OnInsertionChanged() { DeactivateSelection(); const bool activated = ActivateInsertionIfNecessary(); const TouchHandle::AnimationStyle animation = GetAnimationStyle(!activated); insertion_handle_->SetFocus(start_.edge_start(), start_.edge_end()); insertion_handle_->SetVisible(GetStartVisible(), animation); UpdateHandleLayoutIfNecessary(); client_->OnSelectionEvent(activated ? INSERTION_HANDLE_SHOWN : INSERTION_HANDLE_MOVED); } void TouchSelectionController::OnSelectionChanged() { DeactivateInsertion(); const bool activated = ActivateSelectionIfNecessary(); const TouchHandle::AnimationStyle animation = GetAnimationStyle(!activated); start_selection_handle_->SetFocus(start_.edge_start(), start_.edge_end()); end_selection_handle_->SetFocus(end_.edge_start(), end_.edge_end()); start_selection_handle_->SetOrientation(start_orientation_); end_selection_handle_->SetOrientation(end_orientation_); start_selection_handle_->SetVisible(GetStartVisible(), animation); end_selection_handle_->SetVisible(GetEndVisible(), animation); UpdateHandleLayoutIfNecessary(); client_->OnSelectionEvent(activated ? SELECTION_HANDLES_SHOWN : SELECTION_HANDLES_MOVED); } bool TouchSelectionController::ActivateInsertionIfNecessary() { DCHECK_NE(SELECTION_ACTIVE, active_status_); if (!insertion_handle_) { insertion_handle_ = std::make_unique( this, TouchHandleOrientation::CENTER, viewport_rect_); } if (active_status_ == INACTIVE || response_pending_input_event_ == TAP || response_pending_input_event_ == LONG_PRESS) { active_status_ = INSERTION_ACTIVE; insertion_handle_->SetEnabled(true); insertion_handle_->SetViewportRect(viewport_rect_); response_pending_input_event_ = INPUT_EVENT_TYPE_NONE; return true; } return false; } void TouchSelectionController::DeactivateInsertion() { if (active_status_ != INSERTION_ACTIVE) return; DCHECK(insertion_handle_); active_status_ = INACTIVE; insertion_handle_->SetEnabled(false); client_->OnSelectionEvent(INSERTION_HANDLE_CLEARED); } bool TouchSelectionController::ActivateSelectionIfNecessary() { DCHECK_NE(INSERTION_ACTIVE, active_status_); if (!start_selection_handle_) { start_selection_handle_ = std::make_unique(this, start_orientation_, viewport_rect_); } else { start_selection_handle_->SetEnabled(true); start_selection_handle_->SetViewportRect(viewport_rect_); } if (!end_selection_handle_) { end_selection_handle_ = std::make_unique(this, end_orientation_, viewport_rect_); } else { end_selection_handle_->SetEnabled(true); end_selection_handle_->SetViewportRect(viewport_rect_); } // As a long press received while a selection is already active may trigger // an entirely new selection, notify the client but avoid sending an // intervening SELECTION_HANDLES_CLEARED update to avoid unnecessary state // changes. if (active_status_ == INACTIVE || response_pending_input_event_ == LONG_PRESS || response_pending_input_event_ == REPEATED_TAP) { if (active_status_ == SELECTION_ACTIVE) { // The active selection session finishes with the start of the new one. LogSelectionEnd(); } active_status_ = SELECTION_ACTIVE; selection_handle_dragged_ = false; selection_start_time_ = base::TimeTicks::Now(); response_pending_input_event_ = INPUT_EVENT_TYPE_NONE; longpress_drag_selector_.OnSelectionActivated(); return true; } return false; } void TouchSelectionController::DeactivateSelection() { if (active_status_ != SELECTION_ACTIVE) return; DCHECK(start_selection_handle_); DCHECK(end_selection_handle_); LogSelectionEnd(); longpress_drag_selector_.OnSelectionDeactivated(); start_selection_handle_->SetEnabled(false); end_selection_handle_->SetEnabled(false); active_status_ = INACTIVE; client_->OnSelectionEvent(SELECTION_HANDLES_CLEARED); } void TouchSelectionController::UpdateHandleLayoutIfNecessary() { if (active_status_ == INSERTION_ACTIVE) { DCHECK(insertion_handle_); insertion_handle_->UpdateHandleLayout(); } else if (active_status_ == SELECTION_ACTIVE) { DCHECK(start_selection_handle_); DCHECK(end_selection_handle_); start_selection_handle_->UpdateHandleLayout(); end_selection_handle_->UpdateHandleLayout(); } } void TouchSelectionController::RefreshHandleVisibility() { TouchHandle::AnimationStyle animation_style = GetAnimationStyle(true); if (active_status_ == SELECTION_ACTIVE) { start_selection_handle_->SetVisible(GetStartVisible(), animation_style); end_selection_handle_->SetVisible(GetEndVisible(), animation_style); } if (active_status_ == INSERTION_ACTIVE) insertion_handle_->SetVisible(GetStartVisible(), animation_style); // Update handle layout if handle visibility is explicitly changed. UpdateHandleLayoutIfNecessary(); } gfx::Vector2dF TouchSelectionController::GetStartLineOffset() const { return ComputeLineOffsetFromBottom(start_); } gfx::Vector2dF TouchSelectionController::GetEndLineOffset() const { return ComputeLineOffsetFromBottom(end_); } bool TouchSelectionController::GetStartVisible() const { if (!start_.visible()) return false; return !temporarily_hidden_ && !longpress_drag_selector_.IsActive(); } bool TouchSelectionController::GetEndVisible() const { if (!end_.visible()) return false; return !temporarily_hidden_ && !longpress_drag_selector_.IsActive(); } TouchHandle::AnimationStyle TouchSelectionController::GetAnimationStyle( bool was_active) const { return was_active && client_->SupportsAnimation() ? TouchHandle::ANIMATION_SMOOTH : TouchHandle::ANIMATION_NONE; } void TouchSelectionController::LogSelectionEnd() { // TODO(mfomitchev): Once we are able to tell the difference between // 'successful' and 'unsuccessful' selections - log // Event.TouchSelection.Duration instead and get rid of // Event.TouchSelectionD.WasDraggeduration. if (selection_handle_dragged_) { base::TimeDelta duration = base::TimeTicks::Now() - selection_start_time_; UMA_HISTOGRAM_CUSTOM_TIMES("Event.TouchSelection.WasDraggedDuration", duration, base::Milliseconds(500), base::Seconds(60), 60); } } } // namespace ui