Roll src/third_party/WebKit d9c6159:8139f33 (svn 201974:201975)
[chromium-blink-merge.git] / ui / touch_selection / touch_selection_controller.cc
bloba991f94f713c20ba7b7ff4f16f9a3d0120f43cab
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.
5 #include "ui/touch_selection/touch_selection_controller.h"
7 #include "base/auto_reset.h"
8 #include "base/logging.h"
9 #include "base/metrics/histogram_macros.h"
11 namespace ui {
12 namespace {
14 gfx::Vector2dF ComputeLineOffsetFromBottom(const SelectionBound& bound) {
15 gfx::Vector2dF line_offset =
16 gfx::ScaleVector2d(bound.edge_top() - bound.edge_bottom(), 0.5f);
17 // An offset of 8 DIPs is sufficient for most line sizes. For small lines,
18 // using half the line height avoids synthesizing a point on a line above
19 // (or below) the intended line.
20 const gfx::Vector2dF kMaxLineOffset(8.f, 8.f);
21 line_offset.SetToMin(kMaxLineOffset);
22 line_offset.SetToMax(-kMaxLineOffset);
23 return line_offset;
26 TouchHandleOrientation ToTouchHandleOrientation(SelectionBound::Type type) {
27 switch (type) {
28 case SelectionBound::LEFT:
29 return TouchHandleOrientation::LEFT;
30 case SelectionBound::RIGHT:
31 return TouchHandleOrientation::RIGHT;
32 case SelectionBound::CENTER:
33 return TouchHandleOrientation::CENTER;
34 case SelectionBound::EMPTY:
35 return TouchHandleOrientation::UNDEFINED;
37 NOTREACHED() << "Invalid selection bound type: " << type;
38 return TouchHandleOrientation::UNDEFINED;
41 } // namespace
43 TouchSelectionController::Config::Config()
44 : max_tap_duration(base::TimeDelta::FromMilliseconds(300)),
45 tap_slop(8),
46 enable_longpress_drag_selection(false),
47 show_on_tap_for_empty_editable(false) {}
49 TouchSelectionController::Config::~Config() {
52 TouchSelectionController::TouchSelectionController(
53 TouchSelectionControllerClient* client,
54 const Config& config)
55 : client_(client),
56 config_(config),
57 force_next_update_(false),
58 response_pending_input_event_(INPUT_EVENT_TYPE_NONE),
59 start_orientation_(TouchHandleOrientation::UNDEFINED),
60 end_orientation_(TouchHandleOrientation::UNDEFINED),
61 active_status_(INACTIVE),
62 activate_insertion_automatically_(false),
63 activate_selection_automatically_(false),
64 selection_empty_(false),
65 selection_editable_(false),
66 temporarily_hidden_(false),
67 anchor_drag_to_selection_start_(false),
68 longpress_drag_selector_(this),
69 selection_handle_dragged_(false) {
70 DCHECK(client_);
73 TouchSelectionController::~TouchSelectionController() {
76 void TouchSelectionController::OnSelectionBoundsChanged(
77 const SelectionBound& start,
78 const SelectionBound& end) {
79 if (!force_next_update_ && start == start_ && end_ == end)
80 return;
82 // Notify if selection bounds have just been established or dissolved.
83 if (start.type() != SelectionBound::EMPTY &&
84 start_.type() == SelectionBound::EMPTY) {
85 client_->OnSelectionEvent(SELECTION_ESTABLISHED);
86 } else if (start.type() == SelectionBound::EMPTY &&
87 start_.type() != SelectionBound::EMPTY) {
88 client_->OnSelectionEvent(SELECTION_DISSOLVED);
91 start_ = start;
92 end_ = end;
93 start_orientation_ = ToTouchHandleOrientation(start_.type());
94 end_orientation_ = ToTouchHandleOrientation(end_.type());
95 force_next_update_ = false;
97 if (!activate_selection_automatically_ &&
98 !activate_insertion_automatically_) {
99 DCHECK_EQ(INACTIVE, active_status_);
100 DCHECK_EQ(INPUT_EVENT_TYPE_NONE, response_pending_input_event_);
101 return;
104 // Ensure that |response_pending_input_event_| is cleared after the method
105 // completes, while also making its current value available for the duration
106 // of the call.
107 InputEventType causal_input_event = response_pending_input_event_;
108 response_pending_input_event_ = INPUT_EVENT_TYPE_NONE;
109 base::AutoReset<InputEventType> auto_reset_response_pending_input_event(
110 &response_pending_input_event_, causal_input_event);
112 const bool is_selection_dragging = active_status_ == SELECTION_ACTIVE &&
113 (start_selection_handle_->IsActive() ||
114 end_selection_handle_->IsActive());
116 // It's possible that the bounds temporarily overlap while a selection handle
117 // is being dragged, incorrectly reporting a CENTER orientation.
118 // TODO(jdduke): This safeguard is racy, as it's possible the delayed response
119 // from handle positioning occurs *after* the handle dragging has ceased.
120 // Instead, prevent selection -> insertion transitions without an intervening
121 // action or selection clearing of some sort, crbug.com/392696.
122 if (is_selection_dragging) {
123 if (start_orientation_ == TouchHandleOrientation::CENTER)
124 start_orientation_ = start_selection_handle_->orientation();
125 if (end_orientation_ == TouchHandleOrientation::CENTER)
126 end_orientation_ = end_selection_handle_->orientation();
129 if (GetStartPosition() != GetEndPosition() ||
130 (is_selection_dragging &&
131 start_orientation_ != TouchHandleOrientation::UNDEFINED &&
132 end_orientation_ != TouchHandleOrientation::UNDEFINED)) {
133 OnSelectionChanged();
134 return;
137 if (start_orientation_ == TouchHandleOrientation::CENTER &&
138 selection_editable_) {
139 OnInsertionChanged();
140 return;
143 HideAndDisallowShowingAutomatically();
146 bool TouchSelectionController::WillHandleTouchEvent(const MotionEvent& event) {
147 if (config_.enable_longpress_drag_selection &&
148 longpress_drag_selector_.WillHandleTouchEvent(event)) {
149 return true;
152 if (active_status_ == INSERTION_ACTIVE) {
153 DCHECK(insertion_handle_);
154 return insertion_handle_->WillHandleTouchEvent(event);
157 if (active_status_ == SELECTION_ACTIVE) {
158 DCHECK(start_selection_handle_);
159 DCHECK(end_selection_handle_);
160 if (start_selection_handle_->IsActive())
161 return start_selection_handle_->WillHandleTouchEvent(event);
163 if (end_selection_handle_->IsActive())
164 return end_selection_handle_->WillHandleTouchEvent(event);
166 const gfx::PointF event_pos(event.GetX(), event.GetY());
167 if ((event_pos - GetStartPosition()).LengthSquared() <=
168 (event_pos - GetEndPosition()).LengthSquared()) {
169 return start_selection_handle_->WillHandleTouchEvent(event);
171 return end_selection_handle_->WillHandleTouchEvent(event);
174 return false;
177 bool TouchSelectionController::WillHandleTapEvent(const gfx::PointF& location) {
178 if (WillHandleTapOrLongPress(location))
179 return true;
181 response_pending_input_event_ = TAP;
182 if (active_status_ != SELECTION_ACTIVE)
183 activate_selection_automatically_ = false;
184 ShowInsertionHandleAutomatically();
185 if (selection_empty_ && !config_.show_on_tap_for_empty_editable)
186 DeactivateInsertion();
187 ForceNextUpdateIfInactive();
188 return false;
191 bool TouchSelectionController::WillHandleLongPressEvent(
192 base::TimeTicks event_time,
193 const gfx::PointF& location) {
194 if (WillHandleTapOrLongPress(location))
195 return true;
197 longpress_drag_selector_.OnLongPressEvent(event_time, location);
198 response_pending_input_event_ = LONG_PRESS;
199 ShowSelectionHandlesAutomatically();
200 ShowInsertionHandleAutomatically();
201 ForceNextUpdateIfInactive();
202 return false;
205 void TouchSelectionController::AllowShowingFromCurrentSelection() {
206 if (active_status_ != INACTIVE)
207 return;
209 activate_selection_automatically_ = true;
210 activate_insertion_automatically_ = true;
211 if (GetStartPosition() != GetEndPosition()) {
212 OnSelectionChanged();
213 } else if (start_orientation_ == TouchHandleOrientation::CENTER &&
214 selection_editable_) {
215 OnInsertionChanged();
219 void TouchSelectionController::HideAndDisallowShowingAutomatically() {
220 response_pending_input_event_ = INPUT_EVENT_TYPE_NONE;
221 DeactivateInsertion();
222 DeactivateSelection();
223 activate_insertion_automatically_ = false;
224 activate_selection_automatically_ = false;
227 void TouchSelectionController::SetTemporarilyHidden(bool hidden) {
228 if (temporarily_hidden_ == hidden)
229 return;
230 temporarily_hidden_ = hidden;
231 RefreshHandleVisibility();
234 void TouchSelectionController::OnSelectionEditable(bool editable) {
235 if (selection_editable_ == editable)
236 return;
237 selection_editable_ = editable;
238 ForceNextUpdateIfInactive();
239 if (!selection_editable_)
240 DeactivateInsertion();
243 void TouchSelectionController::OnSelectionEmpty(bool empty) {
244 if (selection_empty_ == empty)
245 return;
246 selection_empty_ = empty;
247 ForceNextUpdateIfInactive();
250 bool TouchSelectionController::Animate(base::TimeTicks frame_time) {
251 if (active_status_ == INSERTION_ACTIVE)
252 return insertion_handle_->Animate(frame_time);
254 if (active_status_ == SELECTION_ACTIVE) {
255 bool needs_animate = start_selection_handle_->Animate(frame_time);
256 needs_animate |= end_selection_handle_->Animate(frame_time);
257 return needs_animate;
260 return false;
263 gfx::RectF TouchSelectionController::GetRectBetweenBounds() const {
264 // Short-circuit for efficiency.
265 if (active_status_ == INACTIVE)
266 return gfx::RectF();
268 if (start_.visible() && !end_.visible())
269 return gfx::BoundingRect(start_.edge_top(), start_.edge_bottom());
271 if (end_.visible() && !start_.visible())
272 return gfx::BoundingRect(end_.edge_top(), end_.edge_bottom());
274 // If both handles are visible, or both are invisible, use the entire rect.
275 return RectFBetweenSelectionBounds(start_, end_);
278 gfx::RectF TouchSelectionController::GetStartHandleRect() const {
279 if (active_status_ == INSERTION_ACTIVE)
280 return insertion_handle_->GetVisibleBounds();
281 if (active_status_ == SELECTION_ACTIVE)
282 return start_selection_handle_->GetVisibleBounds();
283 return gfx::RectF();
286 gfx::RectF TouchSelectionController::GetEndHandleRect() const {
287 if (active_status_ == INSERTION_ACTIVE)
288 return insertion_handle_->GetVisibleBounds();
289 if (active_status_ == SELECTION_ACTIVE)
290 return end_selection_handle_->GetVisibleBounds();
291 return gfx::RectF();
294 const gfx::PointF& TouchSelectionController::GetStartPosition() const {
295 return start_.edge_bottom();
298 const gfx::PointF& TouchSelectionController::GetEndPosition() const {
299 return end_.edge_bottom();
302 void TouchSelectionController::OnDragBegin(
303 const TouchSelectionDraggable& draggable,
304 const gfx::PointF& drag_position) {
305 if (&draggable == insertion_handle_.get()) {
306 DCHECK_EQ(active_status_, INSERTION_ACTIVE);
307 client_->OnSelectionEvent(INSERTION_HANDLE_DRAG_STARTED);
308 anchor_drag_to_selection_start_ = true;
309 return;
312 DCHECK_EQ(active_status_, SELECTION_ACTIVE);
314 if (&draggable == start_selection_handle_.get()) {
315 anchor_drag_to_selection_start_ = true;
316 } else if (&draggable == end_selection_handle_.get()) {
317 anchor_drag_to_selection_start_ = false;
318 } else {
319 DCHECK_EQ(&draggable, &longpress_drag_selector_);
320 anchor_drag_to_selection_start_ =
321 (drag_position - GetStartPosition()).LengthSquared() <
322 (drag_position - GetEndPosition()).LengthSquared();
325 gfx::PointF base = GetStartPosition() + GetStartLineOffset();
326 gfx::PointF extent = GetEndPosition() + GetEndLineOffset();
327 if (anchor_drag_to_selection_start_)
328 std::swap(base, extent);
330 selection_handle_dragged_ = true;
332 // When moving the handle we want to move only the extent point. Before doing
333 // so we must make sure that the base point is set correctly.
334 client_->SelectBetweenCoordinates(base, extent);
335 client_->OnSelectionEvent(SELECTION_HANDLE_DRAG_STARTED);
338 void TouchSelectionController::OnDragUpdate(
339 const TouchSelectionDraggable& draggable,
340 const gfx::PointF& drag_position) {
341 // As the position corresponds to the bottom left point of the selection
342 // bound, offset it to some reasonable point on the current line of text.
343 gfx::Vector2dF line_offset = anchor_drag_to_selection_start_
344 ? GetStartLineOffset()
345 : GetEndLineOffset();
346 gfx::PointF line_position = drag_position + line_offset;
347 if (&draggable == insertion_handle_.get())
348 client_->MoveCaret(line_position);
349 else
350 client_->MoveRangeSelectionExtent(line_position);
353 void TouchSelectionController::OnDragEnd(
354 const TouchSelectionDraggable& draggable) {
355 if (&draggable == insertion_handle_.get())
356 client_->OnSelectionEvent(INSERTION_HANDLE_DRAG_STOPPED);
357 else
358 client_->OnSelectionEvent(SELECTION_HANDLE_DRAG_STOPPED);
361 bool TouchSelectionController::IsWithinTapSlop(
362 const gfx::Vector2dF& delta) const {
363 return delta.LengthSquared() <
364 (static_cast<double>(config_.tap_slop) * config_.tap_slop);
367 void TouchSelectionController::OnHandleTapped(const TouchHandle& handle) {
368 if (insertion_handle_ && &handle == insertion_handle_.get())
369 client_->OnSelectionEvent(INSERTION_HANDLE_TAPPED);
372 void TouchSelectionController::SetNeedsAnimate() {
373 client_->SetNeedsAnimate();
376 scoped_ptr<TouchHandleDrawable> TouchSelectionController::CreateDrawable() {
377 return client_->CreateDrawable();
380 base::TimeDelta TouchSelectionController::GetMaxTapDuration() const {
381 return config_.max_tap_duration;
384 void TouchSelectionController::OnLongPressDragActiveStateChanged() {
385 // The handles should remain hidden for the duration of a longpress drag,
386 // including the time between a longpress and the start of drag motion.
387 RefreshHandleVisibility();
390 gfx::PointF TouchSelectionController::GetSelectionStart() const {
391 return GetStartPosition();
394 gfx::PointF TouchSelectionController::GetSelectionEnd() const {
395 return GetEndPosition();
398 void TouchSelectionController::ShowInsertionHandleAutomatically() {
399 if (activate_insertion_automatically_)
400 return;
401 activate_insertion_automatically_ = true;
402 ForceNextUpdateIfInactive();
405 void TouchSelectionController::ShowSelectionHandlesAutomatically() {
406 if (activate_selection_automatically_)
407 return;
408 activate_selection_automatically_ = true;
409 ForceNextUpdateIfInactive();
412 bool TouchSelectionController::WillHandleTapOrLongPress(
413 const gfx::PointF& location) {
414 // If there is an active selection that was not triggered by a user gesture,
415 // allow showing the handles for that selection if a gesture occurs within
416 // the selection rect. Note that this hit test is at best a crude
417 // approximation, and may swallow taps that actually fall outside the
418 // real selection.
419 if (active_status_ == INACTIVE &&
420 GetStartPosition() != GetEndPosition() &&
421 RectFBetweenSelectionBounds(start_, end_).Contains(location)) {
422 AllowShowingFromCurrentSelection();
423 return true;
425 return false;
428 void TouchSelectionController::OnInsertionChanged() {
429 DeactivateSelection();
431 if (response_pending_input_event_ == TAP && selection_empty_ &&
432 !config_.show_on_tap_for_empty_editable) {
433 HideAndDisallowShowingAutomatically();
434 return;
437 if (!activate_insertion_automatically_)
438 return;
440 const bool activated = ActivateInsertionIfNecessary();
442 const TouchHandle::AnimationStyle animation = GetAnimationStyle(!activated);
443 insertion_handle_->SetVisible(GetStartVisible(), animation);
444 insertion_handle_->SetPosition(GetStartPosition());
446 client_->OnSelectionEvent(activated ? INSERTION_HANDLE_SHOWN
447 : INSERTION_HANDLE_MOVED);
450 void TouchSelectionController::OnSelectionChanged() {
451 DeactivateInsertion();
453 if (!activate_selection_automatically_)
454 return;
456 const bool activated = ActivateSelectionIfNecessary();
458 const TouchHandle::AnimationStyle animation = GetAnimationStyle(!activated);
459 start_selection_handle_->SetVisible(GetStartVisible(), animation);
460 end_selection_handle_->SetVisible(GetEndVisible(), animation);
461 start_selection_handle_->SetPosition(GetStartPosition());
462 end_selection_handle_->SetPosition(GetEndPosition());
464 client_->OnSelectionEvent(activated ? SELECTION_HANDLES_SHOWN
465 : SELECTION_HANDLES_MOVED);
468 bool TouchSelectionController::ActivateInsertionIfNecessary() {
469 DCHECK_NE(SELECTION_ACTIVE, active_status_);
471 if (!insertion_handle_) {
472 insertion_handle_.reset(
473 new TouchHandle(this, TouchHandleOrientation::CENTER));
476 if (active_status_ == INACTIVE) {
477 active_status_ = INSERTION_ACTIVE;
478 insertion_handle_->SetEnabled(true);
479 return true;
481 return false;
484 void TouchSelectionController::DeactivateInsertion() {
485 if (active_status_ != INSERTION_ACTIVE)
486 return;
487 DCHECK(insertion_handle_);
488 active_status_ = INACTIVE;
489 insertion_handle_->SetEnabled(false);
490 client_->OnSelectionEvent(INSERTION_HANDLE_CLEARED);
493 bool TouchSelectionController::ActivateSelectionIfNecessary() {
494 DCHECK_NE(INSERTION_ACTIVE, active_status_);
496 if (!start_selection_handle_) {
497 start_selection_handle_.reset(new TouchHandle(this, start_orientation_));
498 } else {
499 start_selection_handle_->SetEnabled(true);
500 start_selection_handle_->SetOrientation(start_orientation_);
503 if (!end_selection_handle_) {
504 end_selection_handle_.reset(new TouchHandle(this, end_orientation_));
505 } else {
506 end_selection_handle_->SetEnabled(true);
507 end_selection_handle_->SetOrientation(end_orientation_);
510 // As a long press received while a selection is already active may trigger
511 // an entirely new selection, notify the client but avoid sending an
512 // intervening SELECTION_HANDLES_CLEARED update to avoid unnecessary state
513 // changes.
514 if (active_status_ == INACTIVE ||
515 response_pending_input_event_ == LONG_PRESS) {
516 if (active_status_ == SELECTION_ACTIVE) {
517 // The active selection session finishes with the start of the new one.
518 LogSelectionEnd();
520 active_status_ = SELECTION_ACTIVE;
521 selection_handle_dragged_ = false;
522 selection_start_time_ = base::TimeTicks::Now();
523 response_pending_input_event_ = INPUT_EVENT_TYPE_NONE;
524 longpress_drag_selector_.OnSelectionActivated();
525 return true;
527 return false;
530 void TouchSelectionController::DeactivateSelection() {
531 if (active_status_ != SELECTION_ACTIVE)
532 return;
533 DCHECK(start_selection_handle_);
534 DCHECK(end_selection_handle_);
535 LogSelectionEnd();
536 longpress_drag_selector_.OnSelectionDeactivated();
537 start_selection_handle_->SetEnabled(false);
538 end_selection_handle_->SetEnabled(false);
539 active_status_ = INACTIVE;
540 client_->OnSelectionEvent(SELECTION_HANDLES_CLEARED);
543 void TouchSelectionController::ForceNextUpdateIfInactive() {
544 // Only force the update if the reported selection is non-empty but still
545 // considered "inactive", i.e., it wasn't preceded by a user gesture or
546 // the handles have since been explicitly hidden.
547 if (active_status_ == INACTIVE &&
548 start_.type() != SelectionBound::EMPTY &&
549 end_.type() != SelectionBound::EMPTY) {
550 force_next_update_ = true;
554 void TouchSelectionController::RefreshHandleVisibility() {
555 TouchHandle::AnimationStyle animation_style = GetAnimationStyle(true);
556 if (active_status_ == SELECTION_ACTIVE) {
557 start_selection_handle_->SetVisible(GetStartVisible(), animation_style);
558 end_selection_handle_->SetVisible(GetEndVisible(), animation_style);
560 if (active_status_ == INSERTION_ACTIVE)
561 insertion_handle_->SetVisible(GetStartVisible(), animation_style);
564 gfx::Vector2dF TouchSelectionController::GetStartLineOffset() const {
565 return ComputeLineOffsetFromBottom(start_);
568 gfx::Vector2dF TouchSelectionController::GetEndLineOffset() const {
569 return ComputeLineOffsetFromBottom(end_);
572 bool TouchSelectionController::GetStartVisible() const {
573 if (!start_.visible())
574 return false;
576 return !temporarily_hidden_ && !longpress_drag_selector_.IsActive();
579 bool TouchSelectionController::GetEndVisible() const {
580 if (!end_.visible())
581 return false;
583 return !temporarily_hidden_ && !longpress_drag_selector_.IsActive();
586 TouchHandle::AnimationStyle TouchSelectionController::GetAnimationStyle(
587 bool was_active) const {
588 return was_active && client_->SupportsAnimation()
589 ? TouchHandle::ANIMATION_SMOOTH
590 : TouchHandle::ANIMATION_NONE;
593 void TouchSelectionController::LogSelectionEnd() {
594 // TODO(mfomitchev): Once we are able to tell the difference between
595 // 'successful' and 'unsuccessful' selections - log
596 // Event.TouchSelection.Duration instead and get rid of
597 // Event.TouchSelectionD.WasDraggeduration.
598 if (selection_handle_dragged_) {
599 base::TimeDelta duration = base::TimeTicks::Now() - selection_start_time_;
600 UMA_HISTOGRAM_CUSTOM_TIMES("Event.TouchSelection.WasDraggedDuration",
601 duration,
602 base::TimeDelta::FromMilliseconds(500),
603 base::TimeDelta::FromSeconds(60),
604 60);
608 } // namespace ui