summaryrefslogtreecommitdiffstats
path: root/ui/touch_selection/longpress_drag_selector.cc
blob: b84c90da9e137cc9f5c068ae75941aa8aaededa6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// 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_ != INACTIVE && state_ != LONGPRESS_PENDING;
}

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::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