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
)
83 // Notify if selection bounds have just been established or dissolved.
84 if (start
.type() != SelectionBound::EMPTY
&&
85 start_
.type() == SelectionBound::EMPTY
) {
86 client_
->OnSelectionEvent(SELECTION_ESTABLISHED
);
87 } else if (start
.type() == SelectionBound::EMPTY
&&
88 start_
.type() != SelectionBound::EMPTY
) {
89 client_
->OnSelectionEvent(SELECTION_DISSOLVED
);
94 start_orientation_
= ToTouchHandleOrientation(start_
.type());
95 end_orientation_
= ToTouchHandleOrientation(end_
.type());
96 force_next_update_
= false;
98 if (!activate_selection_automatically_
&&
99 !activate_insertion_automatically_
) {
100 DCHECK_EQ(INACTIVE
, active_status_
);
101 DCHECK_EQ(INPUT_EVENT_TYPE_NONE
, response_pending_input_event_
);
105 // Ensure that |response_pending_input_event_| is cleared after the method
106 // completes, while also making its current value available for the duration
108 InputEventType causal_input_event
= response_pending_input_event_
;
109 response_pending_input_event_
= INPUT_EVENT_TYPE_NONE
;
110 base::AutoReset
<InputEventType
> auto_reset_response_pending_input_event(
111 &response_pending_input_event_
, causal_input_event
);
113 const bool is_selection_dragging
= active_status_
== SELECTION_ACTIVE
&&
114 (start_selection_handle_
->IsActive() ||
115 end_selection_handle_
->IsActive());
117 // It's possible that the bounds temporarily overlap while a selection handle
118 // is being dragged, incorrectly reporting a CENTER orientation.
119 // TODO(jdduke): This safeguard is racy, as it's possible the delayed response
120 // from handle positioning occurs *after* the handle dragging has ceased.
121 // Instead, prevent selection -> insertion transitions without an intervening
122 // action or selection clearing of some sort, crbug.com/392696.
123 if (is_selection_dragging
) {
124 if (start_orientation_
== TouchHandleOrientation::CENTER
)
125 start_orientation_
= start_selection_handle_
->orientation();
126 if (end_orientation_
== TouchHandleOrientation::CENTER
)
127 end_orientation_
= end_selection_handle_
->orientation();
130 if (GetStartPosition() != GetEndPosition() ||
131 (is_selection_dragging
&&
132 start_orientation_
!= TouchHandleOrientation::UNDEFINED
&&
133 end_orientation_
!= TouchHandleOrientation::UNDEFINED
)) {
134 OnSelectionChanged();
138 if (start_orientation_
== TouchHandleOrientation::CENTER
&&
139 selection_editable_
) {
140 OnInsertionChanged();
144 HideAndDisallowShowingAutomatically();
147 bool TouchSelectionController::WillHandleTouchEvent(const MotionEvent
& event
) {
148 if (config_
.enable_longpress_drag_selection
&&
149 longpress_drag_selector_
.WillHandleTouchEvent(event
)) {
153 if (active_status_
== INSERTION_ACTIVE
) {
154 DCHECK(insertion_handle_
);
155 return insertion_handle_
->WillHandleTouchEvent(event
);
158 if (active_status_
== SELECTION_ACTIVE
) {
159 DCHECK(start_selection_handle_
);
160 DCHECK(end_selection_handle_
);
161 if (start_selection_handle_
->IsActive())
162 return start_selection_handle_
->WillHandleTouchEvent(event
);
164 if (end_selection_handle_
->IsActive())
165 return end_selection_handle_
->WillHandleTouchEvent(event
);
167 const gfx::PointF
event_pos(event
.GetX(), event
.GetY());
168 if ((event_pos
- GetStartPosition()).LengthSquared() <=
169 (event_pos
- GetEndPosition()).LengthSquared()) {
170 return start_selection_handle_
->WillHandleTouchEvent(event
);
172 return end_selection_handle_
->WillHandleTouchEvent(event
);
178 bool TouchSelectionController::WillHandleTapEvent(const gfx::PointF
& location
) {
179 if (WillHandleTapOrLongPress(location
))
182 response_pending_input_event_
= TAP
;
183 if (active_status_
!= SELECTION_ACTIVE
)
184 activate_selection_automatically_
= false;
185 ShowInsertionHandleAutomatically();
186 if (selection_empty_
&& !config_
.show_on_tap_for_empty_editable
)
187 DeactivateInsertion();
188 ForceNextUpdateIfInactive();
192 bool TouchSelectionController::WillHandleLongPressEvent(
193 base::TimeTicks event_time
,
194 const gfx::PointF
& location
) {
195 if (WillHandleTapOrLongPress(location
))
198 longpress_drag_selector_
.OnLongPressEvent(event_time
, location
);
199 response_pending_input_event_
= LONG_PRESS
;
200 ShowSelectionHandlesAutomatically();
201 ShowInsertionHandleAutomatically();
202 ForceNextUpdateIfInactive();
206 void TouchSelectionController::AllowShowingFromCurrentSelection() {
207 if (active_status_
!= INACTIVE
)
210 activate_selection_automatically_
= true;
211 activate_insertion_automatically_
= true;
212 if (GetStartPosition() != GetEndPosition()) {
213 OnSelectionChanged();
214 } else if (start_orientation_
== TouchHandleOrientation::CENTER
&&
215 selection_editable_
) {
216 OnInsertionChanged();
220 void TouchSelectionController::HideAndDisallowShowingAutomatically() {
221 response_pending_input_event_
= INPUT_EVENT_TYPE_NONE
;
222 DeactivateInsertion();
223 DeactivateSelection();
224 activate_insertion_automatically_
= false;
225 activate_selection_automatically_
= false;
228 void TouchSelectionController::SetTemporarilyHidden(bool hidden
) {
229 if (temporarily_hidden_
== hidden
)
231 temporarily_hidden_
= hidden
;
232 RefreshHandleVisibility();
235 void TouchSelectionController::OnSelectionEditable(bool editable
) {
236 if (selection_editable_
== editable
)
238 selection_editable_
= editable
;
239 ForceNextUpdateIfInactive();
240 if (!selection_editable_
)
241 DeactivateInsertion();
244 void TouchSelectionController::OnSelectionEmpty(bool empty
) {
245 if (selection_empty_
== empty
)
247 selection_empty_
= empty
;
248 ForceNextUpdateIfInactive();
251 bool TouchSelectionController::Animate(base::TimeTicks frame_time
) {
252 if (active_status_
== INSERTION_ACTIVE
)
253 return insertion_handle_
->Animate(frame_time
);
255 if (active_status_
== SELECTION_ACTIVE
) {
256 bool needs_animate
= start_selection_handle_
->Animate(frame_time
);
257 needs_animate
|= end_selection_handle_
->Animate(frame_time
);
258 return needs_animate
;
264 gfx::RectF
TouchSelectionController::GetRectBetweenBounds() const {
265 // Short-circuit for efficiency.
266 if (active_status_
== INACTIVE
)
269 if (start_
.visible() && !end_
.visible())
270 return gfx::BoundingRect(start_
.edge_top(), start_
.edge_bottom());
272 if (end_
.visible() && !start_
.visible())
273 return gfx::BoundingRect(end_
.edge_top(), end_
.edge_bottom());
275 // If both handles are visible, or both are invisible, use the entire rect.
276 return RectFBetweenSelectionBounds(start_
, end_
);
279 gfx::RectF
TouchSelectionController::GetStartHandleRect() const {
280 if (active_status_
== INSERTION_ACTIVE
)
281 return insertion_handle_
->GetVisibleBounds();
282 if (active_status_
== SELECTION_ACTIVE
)
283 return start_selection_handle_
->GetVisibleBounds();
287 gfx::RectF
TouchSelectionController::GetEndHandleRect() const {
288 if (active_status_
== INSERTION_ACTIVE
)
289 return insertion_handle_
->GetVisibleBounds();
290 if (active_status_
== SELECTION_ACTIVE
)
291 return end_selection_handle_
->GetVisibleBounds();
295 const gfx::PointF
& TouchSelectionController::GetStartPosition() const {
296 return start_
.edge_bottom();
299 const gfx::PointF
& TouchSelectionController::GetEndPosition() const {
300 return end_
.edge_bottom();
303 void TouchSelectionController::OnDragBegin(
304 const TouchSelectionDraggable
& draggable
,
305 const gfx::PointF
& drag_position
) {
306 if (&draggable
== insertion_handle_
.get()) {
307 DCHECK_EQ(active_status_
, INSERTION_ACTIVE
);
308 client_
->OnSelectionEvent(INSERTION_HANDLE_DRAG_STARTED
);
309 anchor_drag_to_selection_start_
= true;
313 DCHECK_EQ(active_status_
, SELECTION_ACTIVE
);
315 if (&draggable
== start_selection_handle_
.get()) {
316 anchor_drag_to_selection_start_
= true;
317 } else if (&draggable
== end_selection_handle_
.get()) {
318 anchor_drag_to_selection_start_
= false;
320 DCHECK_EQ(&draggable
, &longpress_drag_selector_
);
321 anchor_drag_to_selection_start_
=
322 (drag_position
- GetStartPosition()).LengthSquared() <
323 (drag_position
- GetEndPosition()).LengthSquared();
326 gfx::PointF base
= GetStartPosition() + GetStartLineOffset();
327 gfx::PointF extent
= GetEndPosition() + GetEndLineOffset();
328 if (anchor_drag_to_selection_start_
)
329 std::swap(base
, extent
);
331 selection_handle_dragged_
= true;
333 // When moving the handle we want to move only the extent point. Before doing
334 // so we must make sure that the base point is set correctly.
335 client_
->SelectBetweenCoordinates(base
, extent
);
336 client_
->OnSelectionEvent(SELECTION_HANDLE_DRAG_STARTED
);
339 void TouchSelectionController::OnDragUpdate(
340 const TouchSelectionDraggable
& draggable
,
341 const gfx::PointF
& drag_position
) {
342 // As the position corresponds to the bottom left point of the selection
343 // bound, offset it to some reasonable point on the current line of text.
344 gfx::Vector2dF line_offset
= anchor_drag_to_selection_start_
345 ? GetStartLineOffset()
346 : GetEndLineOffset();
347 gfx::PointF line_position
= drag_position
+ line_offset
;
348 if (&draggable
== insertion_handle_
.get())
349 client_
->MoveCaret(line_position
);
351 client_
->MoveRangeSelectionExtent(line_position
);
354 void TouchSelectionController::OnDragEnd(
355 const TouchSelectionDraggable
& draggable
) {
356 if (&draggable
== insertion_handle_
.get())
357 client_
->OnSelectionEvent(INSERTION_HANDLE_DRAG_STOPPED
);
359 client_
->OnSelectionEvent(SELECTION_HANDLE_DRAG_STOPPED
);
362 bool TouchSelectionController::IsWithinTapSlop(
363 const gfx::Vector2dF
& delta
) const {
364 return delta
.LengthSquared() <
365 (static_cast<double>(config_
.tap_slop
) * config_
.tap_slop
);
368 void TouchSelectionController::OnHandleTapped(const TouchHandle
& handle
) {
369 if (insertion_handle_
&& &handle
== insertion_handle_
.get())
370 client_
->OnSelectionEvent(INSERTION_HANDLE_TAPPED
);
373 void TouchSelectionController::SetNeedsAnimate() {
374 client_
->SetNeedsAnimate();
377 scoped_ptr
<TouchHandleDrawable
> TouchSelectionController::CreateDrawable() {
378 return client_
->CreateDrawable();
381 base::TimeDelta
TouchSelectionController::GetTapTimeout() const {
382 return config_
.tap_timeout
;
385 void TouchSelectionController::OnLongPressDragActiveStateChanged() {
386 // The handles should remain hidden for the duration of a longpress drag,
387 // including the time between a longpress and the start of drag motion.
388 RefreshHandleVisibility();
391 gfx::PointF
TouchSelectionController::GetSelectionStart() const {
392 return GetStartPosition();
395 gfx::PointF
TouchSelectionController::GetSelectionEnd() const {
396 return GetEndPosition();
399 void TouchSelectionController::ShowInsertionHandleAutomatically() {
400 if (activate_insertion_automatically_
)
402 activate_insertion_automatically_
= true;
403 ForceNextUpdateIfInactive();
406 void TouchSelectionController::ShowSelectionHandlesAutomatically() {
407 if (activate_selection_automatically_
)
409 activate_selection_automatically_
= true;
410 ForceNextUpdateIfInactive();
413 bool TouchSelectionController::WillHandleTapOrLongPress(
414 const gfx::PointF
& location
) {
415 // If there is an active selection that was not triggered by a user gesture,
416 // allow showing the handles for that selection if a gesture occurs within
417 // the selection rect. Note that this hit test is at best a crude
418 // approximation, and may swallow taps that actually fall outside the
420 if (active_status_
== INACTIVE
&&
421 GetStartPosition() != GetEndPosition() &&
422 RectFBetweenSelectionBounds(start_
, end_
).Contains(location
)) {
423 AllowShowingFromCurrentSelection();
429 void TouchSelectionController::OnInsertionChanged() {
430 DeactivateSelection();
432 if (response_pending_input_event_
== TAP
&& selection_empty_
&&
433 !config_
.show_on_tap_for_empty_editable
) {
434 HideAndDisallowShowingAutomatically();
438 if (!activate_insertion_automatically_
)
441 const bool activated
= ActivateInsertionIfNecessary();
443 const TouchHandle::AnimationStyle animation
= GetAnimationStyle(!activated
);
444 insertion_handle_
->SetVisible(GetStartVisible(), animation
);
445 insertion_handle_
->SetPosition(GetStartPosition());
447 client_
->OnSelectionEvent(activated
? INSERTION_HANDLE_SHOWN
448 : INSERTION_HANDLE_MOVED
);
451 void TouchSelectionController::OnSelectionChanged() {
452 DeactivateInsertion();
454 if (!activate_selection_automatically_
)
457 const bool activated
= ActivateSelectionIfNecessary();
459 const TouchHandle::AnimationStyle animation
= GetAnimationStyle(!activated
);
460 start_selection_handle_
->SetVisible(GetStartVisible(), animation
);
461 end_selection_handle_
->SetVisible(GetEndVisible(), animation
);
462 start_selection_handle_
->SetPosition(GetStartPosition());
463 end_selection_handle_
->SetPosition(GetEndPosition());
465 client_
->OnSelectionEvent(activated
? SELECTION_HANDLES_SHOWN
466 : SELECTION_HANDLES_MOVED
);
469 bool TouchSelectionController::ActivateInsertionIfNecessary() {
470 DCHECK_NE(SELECTION_ACTIVE
, active_status_
);
472 if (!insertion_handle_
) {
473 insertion_handle_
.reset(
474 new TouchHandle(this, TouchHandleOrientation::CENTER
));
477 if (active_status_
== INACTIVE
) {
478 active_status_
= INSERTION_ACTIVE
;
479 insertion_handle_
->SetEnabled(true);
485 void TouchSelectionController::DeactivateInsertion() {
486 if (active_status_
!= INSERTION_ACTIVE
)
488 DCHECK(insertion_handle_
);
489 active_status_
= INACTIVE
;
490 insertion_handle_
->SetEnabled(false);
491 client_
->OnSelectionEvent(INSERTION_HANDLE_CLEARED
);
494 bool TouchSelectionController::ActivateSelectionIfNecessary() {
495 DCHECK_NE(INSERTION_ACTIVE
, active_status_
);
497 if (!start_selection_handle_
) {
498 start_selection_handle_
.reset(new TouchHandle(this, start_orientation_
));
500 start_selection_handle_
->SetEnabled(true);
501 start_selection_handle_
->SetOrientation(start_orientation_
);
504 if (!end_selection_handle_
) {
505 end_selection_handle_
.reset(new TouchHandle(this, end_orientation_
));
507 end_selection_handle_
->SetEnabled(true);
508 end_selection_handle_
->SetOrientation(end_orientation_
);
511 // As a long press received while a selection is already active may trigger
512 // an entirely new selection, notify the client but avoid sending an
513 // intervening SELECTION_HANDLES_CLEARED update to avoid unnecessary state
515 if (active_status_
== INACTIVE
||
516 response_pending_input_event_
== LONG_PRESS
) {
517 if (active_status_
== SELECTION_ACTIVE
) {
518 // The active selection session finishes with the start of the new one.
521 active_status_
= SELECTION_ACTIVE
;
522 selection_handle_dragged_
= false;
523 selection_start_time_
= base::TimeTicks::Now();
524 response_pending_input_event_
= INPUT_EVENT_TYPE_NONE
;
525 longpress_drag_selector_
.OnSelectionActivated();
531 void TouchSelectionController::DeactivateSelection() {
532 if (active_status_
!= SELECTION_ACTIVE
)
534 DCHECK(start_selection_handle_
);
535 DCHECK(end_selection_handle_
);
537 longpress_drag_selector_
.OnSelectionDeactivated();
538 start_selection_handle_
->SetEnabled(false);
539 end_selection_handle_
->SetEnabled(false);
540 active_status_
= INACTIVE
;
541 client_
->OnSelectionEvent(SELECTION_HANDLES_CLEARED
);
544 void TouchSelectionController::ForceNextUpdateIfInactive() {
545 // Only force the update if the reported selection is non-empty but still
546 // considered "inactive", i.e., it wasn't preceded by a user gesture or
547 // the handles have since been explicitly hidden.
548 if (active_status_
== INACTIVE
&&
549 start_
.type() != SelectionBound::EMPTY
&&
550 end_
.type() != SelectionBound::EMPTY
) {
551 force_next_update_
= true;
555 void TouchSelectionController::RefreshHandleVisibility() {
556 TouchHandle::AnimationStyle animation_style
= GetAnimationStyle(true);
557 if (active_status_
== SELECTION_ACTIVE
) {
558 start_selection_handle_
->SetVisible(GetStartVisible(), animation_style
);
559 end_selection_handle_
->SetVisible(GetEndVisible(), animation_style
);
561 if (active_status_
== INSERTION_ACTIVE
)
562 insertion_handle_
->SetVisible(GetStartVisible(), animation_style
);
565 gfx::Vector2dF
TouchSelectionController::GetStartLineOffset() const {
566 return ComputeLineOffsetFromBottom(start_
);
569 gfx::Vector2dF
TouchSelectionController::GetEndLineOffset() const {
570 return ComputeLineOffsetFromBottom(end_
);
573 bool TouchSelectionController::GetStartVisible() const {
574 if (!start_
.visible())
577 return !temporarily_hidden_
&& !longpress_drag_selector_
.IsActive();
580 bool TouchSelectionController::GetEndVisible() const {
584 return !temporarily_hidden_
&& !longpress_drag_selector_
.IsActive();
587 TouchHandle::AnimationStyle
TouchSelectionController::GetAnimationStyle(
588 bool was_active
) const {
589 return was_active
&& client_
->SupportsAnimation()
590 ? TouchHandle::ANIMATION_SMOOTH
591 : TouchHandle::ANIMATION_NONE
;
594 void TouchSelectionController::LogSelectionEnd() {
595 // TODO(mfomitchev): Once we are able to tell the difference between
596 // 'successful' and 'unsuccessful' selections - log
597 // Event.TouchSelection.Duration instead and get rid of
598 // Event.TouchSelectionD.WasDraggeduration.
599 if (selection_handle_dragged_
) {
600 base::TimeDelta duration
= base::TimeTicks::Now() - selection_start_time_
;
601 UMA_HISTOGRAM_CUSTOM_TIMES("Event.TouchSelection.WasDraggedDuration",
603 base::TimeDelta::FromMilliseconds(500),
604 base::TimeDelta::FromSeconds(60),