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"
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
);
26 TouchHandleOrientation
ToTouchHandleOrientation(SelectionBound::Type 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
;
43 TouchSelectionController::Config::Config()
44 : max_tap_duration(base::TimeDelta::FromMilliseconds(300)),
46 enable_longpress_drag_selection(false),
47 show_on_tap_for_empty_editable(false) {}
49 TouchSelectionController::Config::~Config() {
52 TouchSelectionController::TouchSelectionController(
53 TouchSelectionControllerClient
* client
,
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) {
73 TouchSelectionController::~TouchSelectionController() {
76 void TouchSelectionController::OnSelectionBoundsChanged(
77 const SelectionBound
& start
,
78 const SelectionBound
& end
) {
79 if (!force_next_update_
&& start
== start_
&& end_
== end
)
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
);
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_
);
104 // Ensure that |response_pending_input_event_| is cleared after the method
105 // completes, while also making its current value available for the duration
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();
137 if (start_orientation_
== TouchHandleOrientation::CENTER
&&
138 selection_editable_
) {
139 OnInsertionChanged();
143 HideAndDisallowShowingAutomatically();
146 bool TouchSelectionController::WillHandleTouchEvent(const MotionEvent
& event
) {
147 if (config_
.enable_longpress_drag_selection
&&
148 longpress_drag_selector_
.WillHandleTouchEvent(event
)) {
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
);
177 bool TouchSelectionController::WillHandleTapEvent(const gfx::PointF
& location
) {
178 if (WillHandleTapOrLongPress(location
))
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();
191 bool TouchSelectionController::WillHandleLongPressEvent(
192 base::TimeTicks event_time
,
193 const gfx::PointF
& location
) {
194 if (WillHandleTapOrLongPress(location
))
197 longpress_drag_selector_
.OnLongPressEvent(event_time
, location
);
198 response_pending_input_event_
= LONG_PRESS
;
199 ShowSelectionHandlesAutomatically();
200 ShowInsertionHandleAutomatically();
201 ForceNextUpdateIfInactive();
205 void TouchSelectionController::AllowShowingFromCurrentSelection() {
206 if (active_status_
!= INACTIVE
)
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
)
230 temporarily_hidden_
= hidden
;
231 RefreshHandleVisibility();
234 void TouchSelectionController::OnSelectionEditable(bool editable
) {
235 if (selection_editable_
== editable
)
237 selection_editable_
= editable
;
238 ForceNextUpdateIfInactive();
239 if (!selection_editable_
)
240 DeactivateInsertion();
243 void TouchSelectionController::OnSelectionEmpty(bool empty
) {
244 if (selection_empty_
== empty
)
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
;
263 gfx::RectF
TouchSelectionController::GetRectBetweenBounds() const {
264 // Short-circuit for efficiency.
265 if (active_status_
== INACTIVE
)
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();
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();
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;
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;
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
);
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
);
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_
)
401 activate_insertion_automatically_
= true;
402 ForceNextUpdateIfInactive();
405 void TouchSelectionController::ShowSelectionHandlesAutomatically() {
406 if (activate_selection_automatically_
)
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
419 if (active_status_
== INACTIVE
&&
420 GetStartPosition() != GetEndPosition() &&
421 RectFBetweenSelectionBounds(start_
, end_
).Contains(location
)) {
422 AllowShowingFromCurrentSelection();
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();
437 if (!activate_insertion_automatically_
)
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_
)
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);
484 void TouchSelectionController::DeactivateInsertion() {
485 if (active_status_
!= INSERTION_ACTIVE
)
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_
));
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_
));
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
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.
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();
530 void TouchSelectionController::DeactivateSelection() {
531 if (active_status_
!= SELECTION_ACTIVE
)
533 DCHECK(start_selection_handle_
);
534 DCHECK(end_selection_handle_
);
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())
576 return !temporarily_hidden_
&& !longpress_drag_selector_
.IsActive();
579 bool TouchSelectionController::GetEndVisible() const {
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",
602 base::TimeDelta::FromMilliseconds(500),
603 base::TimeDelta::FromSeconds(60),