1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "content/browser/renderer_host/input/touch_selection_controller.h"
6
7#include "base/auto_reset.h"
8#include "base/logging.h"
9#include "third_party/WebKit/public/web/WebInputEvent.h"
10
11namespace content {
12namespace {
13
14TouchHandleOrientation ToTouchHandleOrientation(cc::SelectionBoundType type) {
15  switch (type) {
16    case cc::SELECTION_BOUND_LEFT:
17      return TOUCH_HANDLE_LEFT;
18    case cc::SELECTION_BOUND_RIGHT:
19      return TOUCH_HANDLE_RIGHT;
20    case cc::SELECTION_BOUND_CENTER:
21      return TOUCH_HANDLE_CENTER;
22    case cc::SELECTION_BOUND_EMPTY:
23      return TOUCH_HANDLE_ORIENTATION_UNDEFINED;
24  }
25  NOTREACHED() << "Invalid selection bound type: " << type;
26  return TOUCH_HANDLE_ORIENTATION_UNDEFINED;
27}
28
29}  // namespace
30
31TouchSelectionController::TouchSelectionController(
32    TouchSelectionControllerClient* client,
33    base::TimeDelta tap_timeout,
34    float tap_slop)
35    : client_(client),
36      tap_timeout_(tap_timeout),
37      tap_slop_(tap_slop),
38      response_pending_input_event_(INPUT_EVENT_TYPE_NONE),
39      start_orientation_(TOUCH_HANDLE_ORIENTATION_UNDEFINED),
40      end_orientation_(TOUCH_HANDLE_ORIENTATION_UNDEFINED),
41      is_insertion_active_(false),
42      activate_insertion_automatically_(false),
43      is_selection_active_(false),
44      activate_selection_automatically_(false),
45      selection_empty_(false),
46      selection_editable_(false),
47      temporarily_hidden_(false) {
48  DCHECK(client_);
49  HideAndDisallowShowingAutomatically();
50}
51
52TouchSelectionController::~TouchSelectionController() {
53}
54
55void TouchSelectionController::OnSelectionBoundsChanged(
56    const cc::ViewportSelectionBound& start,
57    const cc::ViewportSelectionBound& end) {
58  if (!activate_selection_automatically_ &&
59      !activate_insertion_automatically_) {
60    DCHECK_EQ(INPUT_EVENT_TYPE_NONE, response_pending_input_event_);
61    return;
62  }
63
64  if (start == start_ && end_ == end)
65    return;
66
67  start_ = start;
68  end_ = end;
69  start_orientation_ = ToTouchHandleOrientation(start_.type);
70  end_orientation_ = ToTouchHandleOrientation(end_.type);
71
72  // Ensure that |response_pending_input_event_| is cleared after the method
73  // completes, while also making its current value available for the duration
74  // of the call.
75  InputEventType causal_input_event = response_pending_input_event_;
76  response_pending_input_event_ = INPUT_EVENT_TYPE_NONE;
77  base::AutoReset<InputEventType> auto_reset_response_pending_input_event(
78      &response_pending_input_event_, causal_input_event);
79
80  const bool is_selection_dragging =
81      is_selection_active_ && (start_selection_handle_->is_dragging() ||
82                               end_selection_handle_->is_dragging());
83
84  // It's possible that the bounds temporarily overlap while a selection handle
85  // is being dragged, incorrectly reporting a CENTER orientation.
86  // TODO(jdduke): This safeguard is racy, as it's possible the delayed response
87  // from handle positioning occurs *after* the handle dragging has ceased.
88  // Instead, prevent selection -> insertion transitions without an intervening
89  // action or selection clearing of some sort, crbug.com/392696.
90  if (is_selection_dragging) {
91    if (start_orientation_ == TOUCH_HANDLE_CENTER)
92      start_orientation_ = start_selection_handle_->orientation();
93    if (end_orientation_ == TOUCH_HANDLE_CENTER)
94      end_orientation_ = end_selection_handle_->orientation();
95  }
96
97  if (GetStartPosition() != GetEndPosition() ||
98      (is_selection_dragging &&
99       start_orientation_ != TOUCH_HANDLE_ORIENTATION_UNDEFINED &&
100       end_orientation_ != TOUCH_HANDLE_ORIENTATION_UNDEFINED)) {
101    OnSelectionChanged();
102    return;
103  }
104
105  if (start_orientation_ == TOUCH_HANDLE_CENTER && selection_editable_) {
106    OnInsertionChanged();
107    return;
108  }
109
110  HideAndDisallowShowingAutomatically();
111}
112
113bool TouchSelectionController::WillHandleTouchEvent(
114    const ui::MotionEvent& event) {
115  if (is_insertion_active_) {
116    DCHECK(insertion_handle_);
117    return insertion_handle_->WillHandleTouchEvent(event);
118  }
119
120  if (is_selection_active_) {
121    DCHECK(start_selection_handle_);
122    DCHECK(end_selection_handle_);
123    if (start_selection_handle_->is_dragging())
124      return start_selection_handle_->WillHandleTouchEvent(event);
125
126    if (end_selection_handle_->is_dragging())
127      return end_selection_handle_->WillHandleTouchEvent(event);
128
129    const gfx::PointF event_pos(event.GetX(), event.GetY());
130    if ((event_pos - GetStartPosition()).LengthSquared() <=
131        (event_pos - GetEndPosition()).LengthSquared())
132      return start_selection_handle_->WillHandleTouchEvent(event);
133    else
134      return end_selection_handle_->WillHandleTouchEvent(event);
135  }
136
137  return false;
138}
139
140void TouchSelectionController::OnLongPressEvent() {
141  response_pending_input_event_ = LONG_PRESS;
142  ShowSelectionHandlesAutomatically();
143  ShowInsertionHandleAutomatically();
144  ResetCachedValuesIfInactive();
145}
146
147void TouchSelectionController::OnTapEvent() {
148  response_pending_input_event_ = TAP;
149  ShowInsertionHandleAutomatically();
150  if (selection_empty_)
151    DeactivateInsertion();
152  ResetCachedValuesIfInactive();
153}
154
155void TouchSelectionController::HideAndDisallowShowingAutomatically() {
156  response_pending_input_event_ = INPUT_EVENT_TYPE_NONE;
157  DeactivateInsertion();
158  DeactivateSelection();
159  activate_insertion_automatically_ = false;
160  activate_selection_automatically_ = false;
161}
162
163void TouchSelectionController::SetTemporarilyHidden(bool hidden) {
164  if (temporarily_hidden_ == hidden)
165    return;
166  temporarily_hidden_ = hidden;
167
168  TouchHandle::AnimationStyle animation_style = GetAnimationStyle(true);
169  if (is_selection_active_) {
170    start_selection_handle_->SetVisible(GetStartVisible(), animation_style);
171    end_selection_handle_->SetVisible(GetEndVisible(), animation_style);
172  }
173  if (is_insertion_active_)
174    insertion_handle_->SetVisible(GetStartVisible(), animation_style);
175}
176
177void TouchSelectionController::OnSelectionEditable(bool editable) {
178  if (selection_editable_ == editable)
179    return;
180  selection_editable_ = editable;
181  ResetCachedValuesIfInactive();
182  if (!selection_editable_)
183    DeactivateInsertion();
184}
185
186void TouchSelectionController::OnSelectionEmpty(bool empty) {
187  if (selection_empty_ == empty)
188    return;
189  selection_empty_ = empty;
190  ResetCachedValuesIfInactive();
191}
192
193bool TouchSelectionController::Animate(base::TimeTicks frame_time) {
194  if (is_insertion_active_)
195    return insertion_handle_->Animate(frame_time);
196
197  if (is_selection_active_) {
198    bool needs_animate = start_selection_handle_->Animate(frame_time);
199    needs_animate |= end_selection_handle_->Animate(frame_time);
200    return needs_animate;
201  }
202
203  return false;
204}
205
206void TouchSelectionController::OnHandleDragBegin(const TouchHandle& handle) {
207  if (&handle == insertion_handle_.get()) {
208    client_->OnSelectionEvent(INSERTION_DRAG_STARTED, handle.position());
209    return;
210  }
211
212  if (&handle == start_selection_handle_.get()) {
213    fixed_handle_position_ =
214        end_selection_handle_->position() + GetEndLineOffset();
215  } else {
216    fixed_handle_position_ =
217        start_selection_handle_->position() + GetStartLineOffset();
218  }
219  client_->OnSelectionEvent(SELECTION_DRAG_STARTED, handle.position());
220}
221
222void TouchSelectionController::OnHandleDragUpdate(const TouchHandle& handle,
223                                                  const gfx::PointF& position) {
224  // As the position corresponds to the bottom left point of the selection
225  // bound, offset it by half the corresponding line height.
226  gfx::Vector2dF line_offset = &handle == end_selection_handle_.get()
227                                   ? GetStartLineOffset()
228                                   : GetEndLineOffset();
229  gfx::PointF line_position = position + line_offset;
230  if (&handle == insertion_handle_.get()) {
231    client_->MoveCaret(line_position);
232  } else {
233    client_->SelectBetweenCoordinates(fixed_handle_position_, line_position);
234  }
235}
236
237void TouchSelectionController::OnHandleDragEnd(const TouchHandle& handle) {
238  if (&handle != insertion_handle_.get())
239    client_->OnSelectionEvent(SELECTION_DRAG_STOPPED, handle.position());
240}
241
242void TouchSelectionController::OnHandleTapped(const TouchHandle& handle) {
243  if (insertion_handle_ && &handle == insertion_handle_.get())
244    client_->OnSelectionEvent(INSERTION_TAPPED, handle.position());
245}
246
247void TouchSelectionController::SetNeedsAnimate() {
248  client_->SetNeedsAnimate();
249}
250
251scoped_ptr<TouchHandleDrawable> TouchSelectionController::CreateDrawable() {
252  return client_->CreateDrawable();
253}
254
255base::TimeDelta TouchSelectionController::GetTapTimeout() const {
256  return tap_timeout_;
257}
258
259float TouchSelectionController::GetTapSlop() const {
260  return tap_slop_;
261}
262
263void TouchSelectionController::ShowInsertionHandleAutomatically() {
264  if (activate_insertion_automatically_)
265    return;
266  activate_insertion_automatically_ = true;
267  ResetCachedValuesIfInactive();
268}
269
270void TouchSelectionController::ShowSelectionHandlesAutomatically() {
271  if (activate_selection_automatically_)
272    return;
273  activate_selection_automatically_ = true;
274  ResetCachedValuesIfInactive();
275}
276
277void TouchSelectionController::OnInsertionChanged() {
278  DeactivateSelection();
279
280  if (response_pending_input_event_ == TAP && selection_empty_) {
281    HideAndDisallowShowingAutomatically();
282    return;
283  }
284
285  if (!activate_insertion_automatically_)
286    return;
287
288  const bool was_active = is_insertion_active_;
289  const gfx::PointF position = GetStartPosition();
290  if (!is_insertion_active_)
291    ActivateInsertion();
292  else
293    client_->OnSelectionEvent(INSERTION_MOVED, position);
294
295  insertion_handle_->SetVisible(GetStartVisible(),
296                                GetAnimationStyle(was_active));
297  insertion_handle_->SetPosition(position);
298}
299
300void TouchSelectionController::OnSelectionChanged() {
301  DeactivateInsertion();
302
303  if (!activate_selection_automatically_)
304    return;
305
306  const bool was_active = is_selection_active_;
307  ActivateSelection();
308
309  const TouchHandle::AnimationStyle animation = GetAnimationStyle(was_active);
310  start_selection_handle_->SetVisible(GetStartVisible(), animation);
311  end_selection_handle_->SetVisible(GetEndVisible(), animation);
312
313  start_selection_handle_->SetPosition(GetStartPosition());
314  end_selection_handle_->SetPosition(GetEndPosition());
315}
316
317void TouchSelectionController::ActivateInsertion() {
318  DCHECK(!is_selection_active_);
319
320  if (!insertion_handle_)
321    insertion_handle_.reset(new TouchHandle(this, TOUCH_HANDLE_CENTER));
322
323  if (!is_insertion_active_) {
324    is_insertion_active_ = true;
325    insertion_handle_->SetEnabled(true);
326    client_->OnSelectionEvent(INSERTION_SHOWN, GetStartPosition());
327  }
328}
329
330void TouchSelectionController::DeactivateInsertion() {
331  if (!is_insertion_active_)
332    return;
333  DCHECK(insertion_handle_);
334  is_insertion_active_ = false;
335  insertion_handle_->SetEnabled(false);
336  client_->OnSelectionEvent(INSERTION_CLEARED, gfx::PointF());
337}
338
339void TouchSelectionController::ActivateSelection() {
340  DCHECK(!is_insertion_active_);
341
342  if (!start_selection_handle_) {
343    start_selection_handle_.reset(new TouchHandle(this, start_orientation_));
344  } else {
345    start_selection_handle_->SetEnabled(true);
346    start_selection_handle_->SetOrientation(start_orientation_);
347  }
348
349  if (!end_selection_handle_) {
350    end_selection_handle_.reset(new TouchHandle(this, end_orientation_));
351  } else {
352    end_selection_handle_->SetEnabled(true);
353    end_selection_handle_->SetOrientation(end_orientation_);
354  }
355
356  // As a long press received while a selection is already active may trigger
357  // an entirely new selection, notify the client but avoid sending an
358  // intervening SELECTION_CLEARED update to avoid unnecessary state changes.
359  if (!is_selection_active_ || response_pending_input_event_ == LONG_PRESS) {
360    is_selection_active_ = true;
361    response_pending_input_event_ = INPUT_EVENT_TYPE_NONE;
362    client_->OnSelectionEvent(SELECTION_SHOWN, GetStartPosition());
363  }
364}
365
366void TouchSelectionController::DeactivateSelection() {
367  if (!is_selection_active_)
368    return;
369  DCHECK(start_selection_handle_);
370  DCHECK(end_selection_handle_);
371  start_selection_handle_->SetEnabled(false);
372  end_selection_handle_->SetEnabled(false);
373  is_selection_active_ = false;
374  client_->OnSelectionEvent(SELECTION_CLEARED, gfx::PointF());
375}
376
377void TouchSelectionController::ResetCachedValuesIfInactive() {
378  if (is_selection_active_ || is_insertion_active_)
379    return;
380  start_ = cc::ViewportSelectionBound();
381  end_ = cc::ViewportSelectionBound();
382  start_orientation_ = TOUCH_HANDLE_ORIENTATION_UNDEFINED;
383  end_orientation_ = TOUCH_HANDLE_ORIENTATION_UNDEFINED;
384}
385
386const gfx::PointF& TouchSelectionController::GetStartPosition() const {
387  return start_.edge_bottom;
388}
389
390const gfx::PointF& TouchSelectionController::GetEndPosition() const {
391  return end_.edge_bottom;
392}
393
394gfx::Vector2dF TouchSelectionController::GetStartLineOffset() const {
395  return gfx::ScaleVector2d(start_.edge_top - start_.edge_bottom, 0.5f);
396}
397
398gfx::Vector2dF TouchSelectionController::GetEndLineOffset() const {
399  return gfx::ScaleVector2d(end_.edge_top - end_.edge_bottom, 0.5f);
400}
401
402bool TouchSelectionController::GetStartVisible() const {
403  return start_.visible && !temporarily_hidden_;
404}
405
406bool TouchSelectionController::GetEndVisible() const {
407  return end_.visible && !temporarily_hidden_;
408}
409
410TouchHandle::AnimationStyle TouchSelectionController::GetAnimationStyle(
411    bool was_active) const {
412  return was_active && client_->SupportsAnimation()
413             ? TouchHandle::ANIMATION_SMOOTH
414             : TouchHandle::ANIMATION_NONE;
415}
416
417}  // namespace content
418