| // Copyright 2015 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/longpress_drag_selector.h" |
| |
| #include "base/auto_reset.h" |
| #include "ui/events/gesture_detection/motion_event.h" |
| |
| namespace ui { |
| namespace { |
| |
| gfx::Vector2dF SafeNormalize(const gfx::Vector2dF& v) { |
| return v.IsZero() ? v : ScaleVector2d(v, 1.f / v.Length()); |
| } |
| |
| } // namespace |
| |
| LongPressDragSelector::LongPressDragSelector( |
| LongPressDragSelectorClient* client) |
| : client_(client), |
| state_(INACTIVE), |
| has_longpress_drag_start_anchor_(false) { |
| } |
| |
| LongPressDragSelector::~LongPressDragSelector() { |
| } |
| |
| bool LongPressDragSelector::WillHandleTouchEvent(const MotionEvent& event) { |
| switch (event.GetAction()) { |
| case MotionEvent::Action::DOWN: |
| touch_down_position_.SetPoint(event.GetX(), event.GetY()); |
| touch_down_time_ = event.GetEventTime(); |
| has_longpress_drag_start_anchor_ = false; |
| SetState(LONGPRESS_PENDING); |
| return false; |
| |
| case MotionEvent::Action::UP: |
| case MotionEvent::Action::CANCEL: |
| SetState(INACTIVE); |
| return false; |
| |
| case MotionEvent::Action::MOVE: |
| break; |
| |
| default: |
| return false; |
| } |
| |
| if (state_ != DRAG_PENDING && state_ != DRAGGING) |
| return false; |
| |
| gfx::PointF position(event.GetX(), event.GetY()); |
| if (state_ == DRAGGING) { |
| gfx::PointF drag_position = position + longpress_drag_selection_offset_; |
| client_->OnDragUpdate(*this, drag_position); |
| return true; |
| } |
| |
| // We can't use |touch_down_position_| as the offset anchor, as |
| // showing the selection UI may have shifted the motion coordinates. |
| if (!has_longpress_drag_start_anchor_) { |
| has_longpress_drag_start_anchor_ = true; |
| longpress_drag_start_anchor_ = position; |
| return true; |
| } |
| |
| // Allow an additional slop affordance after the longpress occurs. |
| gfx::Vector2dF delta = position - longpress_drag_start_anchor_; |
| if (client_->IsWithinTapSlop(delta)) |
| return true; |
| |
| gfx::PointF selection_start = client_->GetSelectionStart(); |
| gfx::PointF selection_end = client_->GetSelectionEnd(); |
| bool extend_selection_start = false; |
| if (std::abs(delta.y()) > std::abs(delta.x())) { |
| // If initial motion is up/down, extend the start/end selection bound. |
| extend_selection_start = delta.y() < 0; |
| } else { |
| // Otherwise extend the selection bound toward which we're moving, or |
| // the closest bound if motion is already away from both bounds. |
| // Note that, for mixed RTL text, or for multiline selections triggered |
| // by longpress, this may not pick the most suitable drag target |
| gfx::Vector2dF start_delta = selection_start - longpress_drag_start_anchor_; |
| gfx::Vector2dF end_delta = selection_end - longpress_drag_start_anchor_; |
| |
| // The vectors must be normalized to make dot product comparison meaningful. |
| gfx::Vector2dF normalized_start_delta = SafeNormalize(start_delta); |
| gfx::Vector2dF normalized_end_delta = SafeNormalize(end_delta); |
| double start_dot_product = gfx::DotProduct(normalized_start_delta, delta); |
| double end_dot_product = gfx::DotProduct(normalized_end_delta, delta); |
| |
| if (start_dot_product >= 0 || end_dot_product >= 0) { |
| // The greater the dot product the more similar the direction. |
| extend_selection_start = start_dot_product > end_dot_product; |
| } else { |
| // If we're already moving away from both endpoints, pick the closest. |
| extend_selection_start = |
| start_delta.LengthSquared() < end_delta.LengthSquared(); |
| } |
| } |
| |
| gfx::PointF extent = extend_selection_start ? selection_start : selection_end; |
| longpress_drag_selection_offset_ = extent - position; |
| client_->OnDragBegin(*this, extent); |
| SetState(DRAGGING); |
| return true; |
| } |
| |
| bool LongPressDragSelector::IsActive() const { |
| return state_ == DRAG_PENDING || state_ == DRAGGING; |
| } |
| |
| void LongPressDragSelector::OnLongPressEvent(base::TimeTicks event_time, |
| const gfx::PointF& position) { |
| // We have no guarantees that the current gesture stream is aligned with the |
| // observed touch stream. We only know that the gesture sequence is downstream |
| // from the touch sequence. Using a time/distance heuristic helps ensure that |
| // the observed longpress corresponds to the active touch sequence. |
| if (state_ == LONGPRESS_PENDING && |
| // Ensure the down event occurs *before* the longpress event. Use a |
| // small time epsilon to account for floating point time conversion. |
| (touch_down_time_ < event_time + base::TimeDelta::FromMicroseconds(10)) && |
| client_->IsWithinTapSlop(touch_down_position_ - position)) { |
| SetState(SELECTION_PENDING); |
| } |
| } |
| |
| void LongPressDragSelector::OnScrollBeginEvent() { |
| SetState(INACTIVE); |
| } |
| |
| void LongPressDragSelector::OnSelectionActivated() { |
| if (state_ == SELECTION_PENDING) |
| SetState(DRAG_PENDING); |
| } |
| |
| void LongPressDragSelector::OnSelectionDeactivated() { |
| SetState(INACTIVE); |
| } |
| |
| void LongPressDragSelector::SetState(SelectionState state) { |
| if (state_ == state) |
| return; |
| |
| const bool was_dragging = state_ == DRAGGING; |
| const bool was_active = IsActive(); |
| state_ = state; |
| |
| // TODO(jdduke): Add UMA for tracking relative longpress drag frequency. |
| if (was_dragging) |
| client_->OnDragEnd(*this); |
| |
| if (was_active != IsActive()) |
| client_->OnLongPressDragActiveStateChanged(); |
| } |
| |
| } // namespace ui |