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 : tap_timeout(base::TimeDelta::FromMilliseconds(100)),
46 enable_longpress_drag_selection(false),
47 show_on_tap_for_empty_editable(false) {
50 TouchSelectionController::Config::~Config() {
53 TouchSelectionController::TouchSelectionController(
54 TouchSelectionControllerClient
* client
,
58 force_next_update_(false),
59 response_pending_input_event_(INPUT_EVENT_TYPE_NONE
),
60 start_orientation_(TouchHandleOrientation::UNDEFINED
),
61 end_orientation_(TouchHandleOrientation::UNDEFINED
),
62 active_status_(INACTIVE
),
63 activate_insertion_automatically_(false),
64 activate_selection_automatically_(false),
65 selection_empty_(false),
66 selection_editable_(false),
67 temporarily_hidden_(false),
68 anchor_drag_to_selection_start_(false),
69 longpress_drag_selector_(this),
70 selection_handle_dragged_(false) {
74 TouchSelectionController::~TouchSelectionController() {
77 void TouchSelectionController::OnSelectionBoundsChanged(
78 const SelectionBound
& start
,
79 const SelectionBound
& end
) {
80 if (!force_next_update_
&& start
== start_
&& end_
== end
)
85 start_orientation_
= ToTouchHandleOrientation(start_
.type());
86 end_orientation_
= ToTouchHandleOrientation(end_
.type());
87 force_next_update_
= false;
89 if (!activate_selection_automatically_
&&
90 !activate_insertion_automatically_
) {
91 DCHECK_EQ(INACTIVE
, active_status_
);
92 DCHECK_EQ(INPUT_EVENT_TYPE_NONE
, response_pending_input_event_
);
96 // Ensure that |response_pending_input_event_| is cleared after the method
97 // completes, while also making its current value available for the duration
99 InputEventType causal_input_event
= response_pending_input_event_
;
100 response_pending_input_event_
= INPUT_EVENT_TYPE_NONE
;
101 base::AutoReset
<InputEventType
> auto_reset_response_pending_input_event(
102 &response_pending_input_event_
, causal_input_event
);
104 const bool is_selection_dragging
= active_status_
== SELECTION_ACTIVE
&&
105 (start_selection_handle_
->IsActive() ||
106 end_selection_handle_
->IsActive());
108 // It's possible that the bounds temporarily overlap while a selection handle
109 // is being dragged, incorrectly reporting a CENTER orientation.
110 // TODO(jdduke): This safeguard is racy, as it's possible the delayed response
111 // from handle positioning occurs *after* the handle dragging has ceased.
112 // Instead, prevent selection -> insertion transitions without an intervening
113 // action or selection clearing of some sort, crbug.com/392696.
114 if (is_selection_dragging
) {
115 if (start_orientation_
== TouchHandleOrientation::CENTER
)
116 start_orientation_
= start_selection_handle_
->orientation();
117 if (end_orientation_
== TouchHandleOrientation::CENTER
)
118 end_orientation_
= end_selection_handle_
->orientation();
121 if (GetStartPosition() != GetEndPosition() ||
122 (is_selection_dragging
&&
123 start_orientation_
!= TouchHandleOrientation::UNDEFINED
&&
124 end_orientation_
!= TouchHandleOrientation::UNDEFINED
)) {
125 OnSelectionChanged();
129 if (start_orientation_
== TouchHandleOrientation::CENTER
&&
130 selection_editable_
) {
131 OnInsertionChanged();
135 HideAndDisallowShowingAutomatically();
138 bool TouchSelectionController::WillHandleTouchEvent(const MotionEvent
& event
) {
139 if (config_
.enable_longpress_drag_selection
&&
140 longpress_drag_selector_
.WillHandleTouchEvent(event
)) {
144 if (active_status_
== INSERTION_ACTIVE
) {
145 DCHECK(insertion_handle_
);
146 return insertion_handle_
->WillHandleTouchEvent(event
);
149 if (active_status_
== SELECTION_ACTIVE
) {
150 DCHECK(start_selection_handle_
);
151 DCHECK(end_selection_handle_
);
152 if (start_selection_handle_
->IsActive())
153 return start_selection_handle_
->WillHandleTouchEvent(event
);
155 if (end_selection_handle_
->IsActive())
156 return end_selection_handle_
->WillHandleTouchEvent(event
);
158 const gfx::PointF
event_pos(event
.GetX(), event
.GetY());
159 if ((event_pos
- GetStartPosition()).LengthSquared() <=
160 (event_pos
- GetEndPosition()).LengthSquared()) {
161 return start_selection_handle_
->WillHandleTouchEvent(event
);
163 return end_selection_handle_
->WillHandleTouchEvent(event
);
169 bool TouchSelectionController::WillHandleTapEvent(const gfx::PointF
& location
) {
170 if (WillHandleTapOrLongPress(location
))
173 response_pending_input_event_
= TAP
;
174 if (active_status_
!= SELECTION_ACTIVE
)
175 activate_selection_automatically_
= false;
176 ShowInsertionHandleAutomatically();
177 if (selection_empty_
&& !config_
.show_on_tap_for_empty_editable
)
178 DeactivateInsertion();
179 ForceNextUpdateIfInactive();
183 bool TouchSelectionController::WillHandleLongPressEvent(
184 base::TimeTicks event_time
,
185 const gfx::PointF
& location
) {
186 if (WillHandleTapOrLongPress(location
))
189 longpress_drag_selector_
.OnLongPressEvent(event_time
, location
);
190 response_pending_input_event_
= LONG_PRESS
;
191 ShowSelectionHandlesAutomatically();
192 ShowInsertionHandleAutomatically();
193 ForceNextUpdateIfInactive();
197 void TouchSelectionController::AllowShowingFromCurrentSelection() {
198 if (active_status_
!= INACTIVE
)
201 activate_selection_automatically_
= true;
202 activate_insertion_automatically_
= true;
203 if (GetStartPosition() != GetEndPosition()) {
204 OnSelectionChanged();
205 } else if (start_orientation_
== TouchHandleOrientation::CENTER
&&
206 selection_editable_
) {
207 OnInsertionChanged();
211 void TouchSelectionController::HideAndDisallowShowingAutomatically() {
212 response_pending_input_event_
= INPUT_EVENT_TYPE_NONE
;
213 DeactivateInsertion();
214 DeactivateSelection();
215 activate_insertion_automatically_
= false;
216 activate_selection_automatically_
= false;
219 void TouchSelectionController::SetTemporarilyHidden(bool hidden
) {
220 if (temporarily_hidden_
== hidden
)
222 temporarily_hidden_
= hidden
;
223 RefreshHandleVisibility();
226 void TouchSelectionController::OnSelectionEditable(bool editable
) {
227 if (selection_editable_
== editable
)
229 selection_editable_
= editable
;
230 ForceNextUpdateIfInactive();
231 if (!selection_editable_
)
232 DeactivateInsertion();
235 void TouchSelectionController::OnSelectionEmpty(bool empty
) {
236 if (selection_empty_
== empty
)
238 selection_empty_
= empty
;
239 ForceNextUpdateIfInactive();
242 bool TouchSelectionController::Animate(base::TimeTicks frame_time
) {
243 if (active_status_
== INSERTION_ACTIVE
)
244 return insertion_handle_
->Animate(frame_time
);
246 if (active_status_
== SELECTION_ACTIVE
) {
247 bool needs_animate
= start_selection_handle_
->Animate(frame_time
);
248 needs_animate
|= end_selection_handle_
->Animate(frame_time
);
249 return needs_animate
;
255 gfx::RectF
TouchSelectionController::GetRectBetweenBounds() const {
256 // Short-circuit for efficiency.
257 if (active_status_
== INACTIVE
)
260 if (start_
.visible() && !end_
.visible())
261 return gfx::BoundingRect(start_
.edge_top(), start_
.edge_bottom());
263 if (end_
.visible() && !start_
.visible())
264 return gfx::BoundingRect(end_
.edge_top(), end_
.edge_bottom());
266 // If both handles are visible, or both are invisible, use the entire rect.
267 return RectFBetweenSelectionBounds(start_
, end_
);
270 gfx::RectF
TouchSelectionController::GetStartHandleRect() const {
271 if (active_status_
== INSERTION_ACTIVE
)
272 return insertion_handle_
->GetVisibleBounds();
273 if (active_status_
== SELECTION_ACTIVE
)
274 return start_selection_handle_
->GetVisibleBounds();
278 gfx::RectF
TouchSelectionController::GetEndHandleRect() const {
279 if (active_status_
== INSERTION_ACTIVE
)
280 return insertion_handle_
->GetVisibleBounds();
281 if (active_status_
== SELECTION_ACTIVE
)
282 return end_selection_handle_
->GetVisibleBounds();
286 const gfx::PointF
& TouchSelectionController::GetStartPosition() const {
287 return start_
.edge_bottom();
290 const gfx::PointF
& TouchSelectionController::GetEndPosition() const {
291 return end_
.edge_bottom();
294 void TouchSelectionController::OnDragBegin(
295 const TouchSelectionDraggable
& draggable
,
296 const gfx::PointF
& drag_position
) {
297 if (&draggable
== insertion_handle_
.get()) {
298 DCHECK_EQ(active_status_
, INSERTION_ACTIVE
);
299 client_
->OnSelectionEvent(INSERTION_DRAG_STARTED
);
300 anchor_drag_to_selection_start_
= true;
304 DCHECK_EQ(active_status_
, SELECTION_ACTIVE
);
306 if (&draggable
== start_selection_handle_
.get()) {
307 anchor_drag_to_selection_start_
= true;
308 } else if (&draggable
== end_selection_handle_
.get()) {
309 anchor_drag_to_selection_start_
= false;
311 DCHECK_EQ(&draggable
, &longpress_drag_selector_
);
312 anchor_drag_to_selection_start_
=
313 (drag_position
- GetStartPosition()).LengthSquared() <
314 (drag_position
- GetEndPosition()).LengthSquared();
317 gfx::PointF base
= GetStartPosition() + GetStartLineOffset();
318 gfx::PointF extent
= GetEndPosition() + GetEndLineOffset();
319 if (anchor_drag_to_selection_start_
)
320 std::swap(base
, extent
);
322 selection_handle_dragged_
= true;
324 // When moving the handle we want to move only the extent point. Before doing
325 // so we must make sure that the base point is set correctly.
326 client_
->SelectBetweenCoordinates(base
, extent
);
327 client_
->OnSelectionEvent(SELECTION_DRAG_STARTED
);
330 void TouchSelectionController::OnDragUpdate(
331 const TouchSelectionDraggable
& draggable
,
332 const gfx::PointF
& drag_position
) {
333 // As the position corresponds to the bottom left point of the selection
334 // bound, offset it to some reasonable point on the current line of text.
335 gfx::Vector2dF line_offset
= anchor_drag_to_selection_start_
336 ? GetStartLineOffset()
337 : GetEndLineOffset();
338 gfx::PointF line_position
= drag_position
+ line_offset
;
339 if (&draggable
== insertion_handle_
.get())
340 client_
->MoveCaret(line_position
);
342 client_
->MoveRangeSelectionExtent(line_position
);
345 void TouchSelectionController::OnDragEnd(
346 const TouchSelectionDraggable
& draggable
) {
347 if (&draggable
== insertion_handle_
.get())
348 client_
->OnSelectionEvent(INSERTION_DRAG_STOPPED
);
350 client_
->OnSelectionEvent(SELECTION_DRAG_STOPPED
);
353 bool TouchSelectionController::IsWithinTapSlop(
354 const gfx::Vector2dF
& delta
) const {
355 return delta
.LengthSquared() <
356 (static_cast<double>(config_
.tap_slop
) * config_
.tap_slop
);
359 void TouchSelectionController::OnHandleTapped(const TouchHandle
& handle
) {
360 if (insertion_handle_
&& &handle
== insertion_handle_
.get())
361 client_
->OnSelectionEvent(INSERTION_TAPPED
);
364 void TouchSelectionController::SetNeedsAnimate() {
365 client_
->SetNeedsAnimate();
368 scoped_ptr
<TouchHandleDrawable
> TouchSelectionController::CreateDrawable() {
369 return client_
->CreateDrawable();
372 base::TimeDelta
TouchSelectionController::GetTapTimeout() const {
373 return config_
.tap_timeout
;
376 void TouchSelectionController::OnLongPressDragActiveStateChanged() {
377 // The handles should remain hidden for the duration of a longpress drag,
378 // including the time between a longpress and the start of drag motion.
379 RefreshHandleVisibility();
382 gfx::PointF
TouchSelectionController::GetSelectionStart() const {
383 return GetStartPosition();
386 gfx::PointF
TouchSelectionController::GetSelectionEnd() const {
387 return GetEndPosition();
390 void TouchSelectionController::ShowInsertionHandleAutomatically() {
391 if (activate_insertion_automatically_
)
393 activate_insertion_automatically_
= true;
394 ForceNextUpdateIfInactive();
397 void TouchSelectionController::ShowSelectionHandlesAutomatically() {
398 if (activate_selection_automatically_
)
400 activate_selection_automatically_
= true;
401 ForceNextUpdateIfInactive();
404 bool TouchSelectionController::WillHandleTapOrLongPress(
405 const gfx::PointF
& location
) {
406 // If there is an active selection that was not triggered by a user gesture,
407 // allow showing the handles for that selection if a gesture occurs within
408 // the selection rect. Note that this hit test is at best a crude
409 // approximation, and may swallow taps that actually fall outside the
411 if (active_status_
== INACTIVE
&&
412 GetStartPosition() != GetEndPosition() &&
413 RectFBetweenSelectionBounds(start_
, end_
).Contains(location
)) {
414 AllowShowingFromCurrentSelection();
420 void TouchSelectionController::OnInsertionChanged() {
421 DeactivateSelection();
423 if (response_pending_input_event_
== TAP
&& selection_empty_
&&
424 !config_
.show_on_tap_for_empty_editable
) {
425 HideAndDisallowShowingAutomatically();
429 if (!activate_insertion_automatically_
)
432 const bool activated
= ActivateInsertionIfNecessary();
434 const TouchHandle::AnimationStyle animation
= GetAnimationStyle(!activated
);
435 insertion_handle_
->SetVisible(GetStartVisible(), animation
);
436 insertion_handle_
->SetPosition(GetStartPosition());
438 client_
->OnSelectionEvent(activated
? INSERTION_SHOWN
: INSERTION_MOVED
);
441 void TouchSelectionController::OnSelectionChanged() {
442 DeactivateInsertion();
444 if (!activate_selection_automatically_
)
447 const bool activated
= ActivateSelectionIfNecessary();
449 const TouchHandle::AnimationStyle animation
= GetAnimationStyle(!activated
);
450 start_selection_handle_
->SetVisible(GetStartVisible(), animation
);
451 end_selection_handle_
->SetVisible(GetEndVisible(), animation
);
452 start_selection_handle_
->SetPosition(GetStartPosition());
453 end_selection_handle_
->SetPosition(GetEndPosition());
455 client_
->OnSelectionEvent(activated
? SELECTION_SHOWN
: SELECTION_MOVED
);
458 bool TouchSelectionController::ActivateInsertionIfNecessary() {
459 DCHECK_NE(SELECTION_ACTIVE
, active_status_
);
461 if (!insertion_handle_
) {
462 insertion_handle_
.reset(
463 new TouchHandle(this, TouchHandleOrientation::CENTER
));
466 if (active_status_
== INACTIVE
) {
467 active_status_
= INSERTION_ACTIVE
;
468 insertion_handle_
->SetEnabled(true);
474 void TouchSelectionController::DeactivateInsertion() {
475 if (active_status_
!= INSERTION_ACTIVE
)
477 DCHECK(insertion_handle_
);
478 active_status_
= INACTIVE
;
479 insertion_handle_
->SetEnabled(false);
480 client_
->OnSelectionEvent(INSERTION_CLEARED
);
483 bool TouchSelectionController::ActivateSelectionIfNecessary() {
484 DCHECK_NE(INSERTION_ACTIVE
, active_status_
);
486 if (!start_selection_handle_
) {
487 start_selection_handle_
.reset(new TouchHandle(this, start_orientation_
));
489 start_selection_handle_
->SetEnabled(true);
490 start_selection_handle_
->SetOrientation(start_orientation_
);
493 if (!end_selection_handle_
) {
494 end_selection_handle_
.reset(new TouchHandle(this, end_orientation_
));
496 end_selection_handle_
->SetEnabled(true);
497 end_selection_handle_
->SetOrientation(end_orientation_
);
500 // As a long press received while a selection is already active may trigger
501 // an entirely new selection, notify the client but avoid sending an
502 // intervening SELECTION_CLEARED update to avoid unnecessary state changes.
503 if (active_status_
== INACTIVE
||
504 response_pending_input_event_
== LONG_PRESS
) {
505 if (active_status_
== SELECTION_ACTIVE
) {
506 // The active selection session finishes with the start of the new one.
509 active_status_
= SELECTION_ACTIVE
;
510 selection_handle_dragged_
= false;
511 selection_start_time_
= base::TimeTicks::Now();
512 response_pending_input_event_
= INPUT_EVENT_TYPE_NONE
;
513 longpress_drag_selector_
.OnSelectionActivated();
519 void TouchSelectionController::DeactivateSelection() {
520 if (active_status_
!= SELECTION_ACTIVE
)
522 DCHECK(start_selection_handle_
);
523 DCHECK(end_selection_handle_
);
525 longpress_drag_selector_
.OnSelectionDeactivated();
526 start_selection_handle_
->SetEnabled(false);
527 end_selection_handle_
->SetEnabled(false);
528 active_status_
= INACTIVE
;
529 client_
->OnSelectionEvent(SELECTION_CLEARED
);
532 void TouchSelectionController::ForceNextUpdateIfInactive() {
533 // Only force the update if the reported selection is non-empty but still
534 // considered "inactive", i.e., it wasn't preceded by a user gesture or
535 // the handles have since been explicitly hidden.
536 if (active_status_
== INACTIVE
&&
537 start_
.type() != SelectionBound::EMPTY
&&
538 end_
.type() != SelectionBound::EMPTY
) {
539 force_next_update_
= true;
543 void TouchSelectionController::RefreshHandleVisibility() {
544 TouchHandle::AnimationStyle animation_style
= GetAnimationStyle(true);
545 if (active_status_
== SELECTION_ACTIVE
) {
546 start_selection_handle_
->SetVisible(GetStartVisible(), animation_style
);
547 end_selection_handle_
->SetVisible(GetEndVisible(), animation_style
);
549 if (active_status_
== INSERTION_ACTIVE
)
550 insertion_handle_
->SetVisible(GetStartVisible(), animation_style
);
553 gfx::Vector2dF
TouchSelectionController::GetStartLineOffset() const {
554 return ComputeLineOffsetFromBottom(start_
);
557 gfx::Vector2dF
TouchSelectionController::GetEndLineOffset() const {
558 return ComputeLineOffsetFromBottom(end_
);
561 bool TouchSelectionController::GetStartVisible() const {
562 if (!start_
.visible())
565 return !temporarily_hidden_
&& !longpress_drag_selector_
.IsActive();
568 bool TouchSelectionController::GetEndVisible() const {
572 return !temporarily_hidden_
&& !longpress_drag_selector_
.IsActive();
575 TouchHandle::AnimationStyle
TouchSelectionController::GetAnimationStyle(
576 bool was_active
) const {
577 return was_active
&& client_
->SupportsAnimation()
578 ? TouchHandle::ANIMATION_SMOOTH
579 : TouchHandle::ANIMATION_NONE
;
582 void TouchSelectionController::LogSelectionEnd() {
583 // TODO(mfomitchev): Once we are able to tell the difference between
584 // 'successful' and 'unsuccessful' selections - log
585 // Event.TouchSelection.Duration instead and get rid of
586 // Event.TouchSelectionD.WasDraggeduration.
587 if (selection_handle_dragged_
) {
588 base::TimeDelta duration
= base::TimeTicks::Now() - selection_start_time_
;
589 UMA_HISTOGRAM_CUSTOM_TIMES("Event.TouchSelection.WasDraggedDuration",
591 base::TimeDelta::FromMilliseconds(500),
592 base::TimeDelta::FromSeconds(60),