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 5 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(5.f
, 5.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::TouchSelectionController(
44 TouchSelectionControllerClient
* client
,
45 base::TimeDelta tap_timeout
,
47 bool show_on_tap_for_empty_editable
)
49 tap_timeout_(tap_timeout
),
51 show_on_tap_for_empty_editable_(show_on_tap_for_empty_editable
),
52 response_pending_input_event_(INPUT_EVENT_TYPE_NONE
),
53 start_orientation_(TouchHandleOrientation::UNDEFINED
),
54 end_orientation_(TouchHandleOrientation::UNDEFINED
),
55 active_status_(INACTIVE
),
56 activate_insertion_automatically_(false),
57 activate_selection_automatically_(false),
58 selection_empty_(false),
59 selection_editable_(false),
60 temporarily_hidden_(false),
61 selection_handle_dragged_(false) {
65 TouchSelectionController::~TouchSelectionController() {
68 void TouchSelectionController::OnSelectionBoundsChanged(
69 const SelectionBound
& start
,
70 const SelectionBound
& end
) {
71 if (start
== start_
&& end_
== end
)
76 start_orientation_
= ToTouchHandleOrientation(start_
.type());
77 end_orientation_
= ToTouchHandleOrientation(end_
.type());
79 if (!activate_selection_automatically_
&&
80 !activate_insertion_automatically_
) {
81 DCHECK_EQ(INACTIVE
, active_status_
);
82 DCHECK_EQ(INPUT_EVENT_TYPE_NONE
, response_pending_input_event_
);
86 // Ensure that |response_pending_input_event_| is cleared after the method
87 // completes, while also making its current value available for the duration
89 InputEventType causal_input_event
= response_pending_input_event_
;
90 response_pending_input_event_
= INPUT_EVENT_TYPE_NONE
;
91 base::AutoReset
<InputEventType
> auto_reset_response_pending_input_event(
92 &response_pending_input_event_
, causal_input_event
);
94 const bool is_selection_dragging
= active_status_
== SELECTION_ACTIVE
&&
95 (start_selection_handle_
->is_dragging() ||
96 end_selection_handle_
->is_dragging());
98 // It's possible that the bounds temporarily overlap while a selection handle
99 // is being dragged, incorrectly reporting a CENTER orientation.
100 // TODO(jdduke): This safeguard is racy, as it's possible the delayed response
101 // from handle positioning occurs *after* the handle dragging has ceased.
102 // Instead, prevent selection -> insertion transitions without an intervening
103 // action or selection clearing of some sort, crbug.com/392696.
104 if (is_selection_dragging
) {
105 if (start_orientation_
== TouchHandleOrientation::CENTER
)
106 start_orientation_
= start_selection_handle_
->orientation();
107 if (end_orientation_
== TouchHandleOrientation::CENTER
)
108 end_orientation_
= end_selection_handle_
->orientation();
111 if (GetStartPosition() != GetEndPosition() ||
112 (is_selection_dragging
&&
113 start_orientation_
!= TouchHandleOrientation::UNDEFINED
&&
114 end_orientation_
!= TouchHandleOrientation::UNDEFINED
)) {
115 OnSelectionChanged();
119 if (start_orientation_
== TouchHandleOrientation::CENTER
&&
120 selection_editable_
) {
121 OnInsertionChanged();
125 HideAndDisallowShowingAutomatically();
128 bool TouchSelectionController::WillHandleTouchEvent(const MotionEvent
& event
) {
129 if (active_status_
== INSERTION_ACTIVE
) {
130 DCHECK(insertion_handle_
);
131 return insertion_handle_
->WillHandleTouchEvent(event
);
134 if (active_status_
== SELECTION_ACTIVE
) {
135 DCHECK(start_selection_handle_
);
136 DCHECK(end_selection_handle_
);
137 if (start_selection_handle_
->is_dragging())
138 return start_selection_handle_
->WillHandleTouchEvent(event
);
140 if (end_selection_handle_
->is_dragging())
141 return end_selection_handle_
->WillHandleTouchEvent(event
);
143 const gfx::PointF
event_pos(event
.GetX(), event
.GetY());
144 if ((event_pos
- GetStartPosition()).LengthSquared() <=
145 (event_pos
- GetEndPosition()).LengthSquared()) {
146 return start_selection_handle_
->WillHandleTouchEvent(event
);
148 return end_selection_handle_
->WillHandleTouchEvent(event
);
154 void TouchSelectionController::OnLongPressEvent() {
155 response_pending_input_event_
= LONG_PRESS
;
156 ShowSelectionHandlesAutomatically();
157 ShowInsertionHandleAutomatically();
158 ResetCachedValuesIfInactive();
161 void TouchSelectionController::AllowShowingFromCurrentSelection() {
162 if (active_status_
!= INACTIVE
)
165 activate_selection_automatically_
= true;
166 activate_insertion_automatically_
= true;
167 if (GetStartPosition() != GetEndPosition()) {
168 OnSelectionChanged();
169 } else if (start_orientation_
== TouchHandleOrientation::CENTER
&&
170 selection_editable_
) {
171 OnInsertionChanged();
175 void TouchSelectionController::OnTapEvent() {
176 response_pending_input_event_
= TAP
;
177 if (active_status_
!= SELECTION_ACTIVE
)
178 activate_selection_automatically_
= false;
179 ShowInsertionHandleAutomatically();
180 if (selection_empty_
&& !show_on_tap_for_empty_editable_
)
181 DeactivateInsertion();
182 ResetCachedValuesIfInactive();
185 void TouchSelectionController::HideAndDisallowShowingAutomatically() {
186 response_pending_input_event_
= INPUT_EVENT_TYPE_NONE
;
187 DeactivateInsertion();
188 DeactivateSelection();
189 activate_insertion_automatically_
= false;
190 activate_selection_automatically_
= false;
193 void TouchSelectionController::SetTemporarilyHidden(bool hidden
) {
194 if (temporarily_hidden_
== hidden
)
196 temporarily_hidden_
= hidden
;
198 TouchHandle::AnimationStyle animation_style
= GetAnimationStyle(true);
199 if (active_status_
== SELECTION_ACTIVE
) {
200 start_selection_handle_
->SetVisible(GetStartVisible(), animation_style
);
201 end_selection_handle_
->SetVisible(GetEndVisible(), animation_style
);
202 } else if (active_status_
== INSERTION_ACTIVE
) {
203 insertion_handle_
->SetVisible(GetStartVisible(), animation_style
);
207 void TouchSelectionController::OnSelectionEditable(bool editable
) {
208 if (selection_editable_
== editable
)
210 selection_editable_
= editable
;
211 ResetCachedValuesIfInactive();
212 if (!selection_editable_
)
213 DeactivateInsertion();
216 void TouchSelectionController::OnSelectionEmpty(bool empty
) {
217 if (selection_empty_
== empty
)
219 selection_empty_
= empty
;
220 ResetCachedValuesIfInactive();
223 bool TouchSelectionController::Animate(base::TimeTicks frame_time
) {
224 if (active_status_
== INSERTION_ACTIVE
)
225 return insertion_handle_
->Animate(frame_time
);
227 if (active_status_
== SELECTION_ACTIVE
) {
228 bool needs_animate
= start_selection_handle_
->Animate(frame_time
);
229 needs_animate
|= end_selection_handle_
->Animate(frame_time
);
230 return needs_animate
;
236 gfx::RectF
TouchSelectionController::GetRectBetweenBounds() const {
237 // Short-circuit for efficiency.
238 if (active_status_
== INACTIVE
)
241 if (start_
.visible() && !end_
.visible())
242 return gfx::BoundingRect(start_
.edge_top(), start_
.edge_bottom());
244 if (end_
.visible() && !start_
.visible())
245 return gfx::BoundingRect(end_
.edge_top(), end_
.edge_bottom());
247 // If both handles are visible, or both are invisible, use the entire rect.
248 return RectFBetweenSelectionBounds(start_
, end_
);
251 gfx::RectF
TouchSelectionController::GetStartHandleRect() const {
252 if (active_status_
== INSERTION_ACTIVE
)
253 return insertion_handle_
->GetVisibleBounds();
254 if (active_status_
== SELECTION_ACTIVE
)
255 return start_selection_handle_
->GetVisibleBounds();
259 gfx::RectF
TouchSelectionController::GetEndHandleRect() const {
260 if (active_status_
== INSERTION_ACTIVE
)
261 return insertion_handle_
->GetVisibleBounds();
262 if (active_status_
== SELECTION_ACTIVE
)
263 return end_selection_handle_
->GetVisibleBounds();
267 const gfx::PointF
& TouchSelectionController::GetStartPosition() const {
268 return start_
.edge_bottom();
271 const gfx::PointF
& TouchSelectionController::GetEndPosition() const {
272 return end_
.edge_bottom();
275 void TouchSelectionController::OnHandleDragBegin(const TouchHandle
& handle
) {
276 if (&handle
== insertion_handle_
.get()) {
277 client_
->OnSelectionEvent(INSERTION_DRAG_STARTED
);
281 gfx::PointF base
, extent
;
282 if (&handle
== start_selection_handle_
.get()) {
283 base
= end_selection_handle_
->position() + GetEndLineOffset();
284 extent
= start_selection_handle_
->position() + GetStartLineOffset();
286 base
= start_selection_handle_
->position() + GetStartLineOffset();
287 extent
= end_selection_handle_
->position() + GetEndLineOffset();
289 selection_handle_dragged_
= true;
291 // When moving the handle we want to move only the extent point. Before doing
292 // so we must make sure that the base point is set correctly.
293 client_
->SelectBetweenCoordinates(base
, extent
);
294 client_
->OnSelectionEvent(SELECTION_DRAG_STARTED
);
297 void TouchSelectionController::OnHandleDragUpdate(const TouchHandle
& handle
,
298 const gfx::PointF
& position
) {
299 // As the position corresponds to the bottom left point of the selection
300 // bound, offset it by half the corresponding line height.
301 gfx::Vector2dF line_offset
= &handle
== start_selection_handle_
.get()
302 ? GetStartLineOffset()
303 : GetEndLineOffset();
304 gfx::PointF line_position
= position
+ line_offset
;
305 if (&handle
== insertion_handle_
.get())
306 client_
->MoveCaret(line_position
);
308 client_
->MoveRangeSelectionExtent(line_position
);
311 void TouchSelectionController::OnHandleDragEnd(const TouchHandle
& handle
) {
312 if (&handle
== insertion_handle_
.get())
313 client_
->OnSelectionEvent(INSERTION_DRAG_STOPPED
);
315 client_
->OnSelectionEvent(SELECTION_DRAG_STOPPED
);
318 void TouchSelectionController::OnHandleTapped(const TouchHandle
& handle
) {
319 if (insertion_handle_
&& &handle
== insertion_handle_
.get())
320 client_
->OnSelectionEvent(INSERTION_TAPPED
);
323 void TouchSelectionController::SetNeedsAnimate() {
324 client_
->SetNeedsAnimate();
327 scoped_ptr
<TouchHandleDrawable
> TouchSelectionController::CreateDrawable() {
328 return client_
->CreateDrawable();
331 base::TimeDelta
TouchSelectionController::GetTapTimeout() const {
335 float TouchSelectionController::GetTapSlop() const {
339 void TouchSelectionController::ShowInsertionHandleAutomatically() {
340 if (activate_insertion_automatically_
)
342 activate_insertion_automatically_
= true;
343 ResetCachedValuesIfInactive();
346 void TouchSelectionController::ShowSelectionHandlesAutomatically() {
347 if (activate_selection_automatically_
)
349 activate_selection_automatically_
= true;
350 ResetCachedValuesIfInactive();
353 void TouchSelectionController::OnInsertionChanged() {
354 DeactivateSelection();
356 if (response_pending_input_event_
== TAP
&& selection_empty_
&&
357 !show_on_tap_for_empty_editable_
) {
358 HideAndDisallowShowingAutomatically();
362 if (!activate_insertion_automatically_
)
365 const bool was_active
= active_status_
== INSERTION_ACTIVE
;
366 const gfx::PointF position
= GetStartPosition();
370 client_
->OnSelectionEvent(INSERTION_MOVED
);
372 insertion_handle_
->SetVisible(GetStartVisible(),
373 GetAnimationStyle(was_active
));
374 insertion_handle_
->SetPosition(position
);
377 void TouchSelectionController::OnSelectionChanged() {
378 DeactivateInsertion();
380 if (!activate_selection_automatically_
)
383 const bool was_active
= active_status_
== SELECTION_ACTIVE
;
384 if (!was_active
|| response_pending_input_event_
== LONG_PRESS
)
387 client_
->OnSelectionEvent(SELECTION_MOVED
);
389 const TouchHandle::AnimationStyle animation
= GetAnimationStyle(was_active
);
390 start_selection_handle_
->SetVisible(GetStartVisible(), animation
);
391 end_selection_handle_
->SetVisible(GetEndVisible(), animation
);
393 start_selection_handle_
->SetPosition(GetStartPosition());
394 end_selection_handle_
->SetPosition(GetEndPosition());
397 void TouchSelectionController::ActivateInsertion() {
398 DCHECK_NE(SELECTION_ACTIVE
, active_status_
);
400 if (!insertion_handle_
)
401 insertion_handle_
.reset(
402 new TouchHandle(this, TouchHandleOrientation::CENTER
));
404 if (active_status_
== INACTIVE
) {
405 active_status_
= INSERTION_ACTIVE
;
406 insertion_handle_
->SetEnabled(true);
407 client_
->OnSelectionEvent(INSERTION_SHOWN
);
411 void TouchSelectionController::DeactivateInsertion() {
412 if (active_status_
!= INSERTION_ACTIVE
)
414 DCHECK(insertion_handle_
);
415 active_status_
= INACTIVE
;
416 insertion_handle_
->SetEnabled(false);
417 client_
->OnSelectionEvent(INSERTION_CLEARED
);
420 void TouchSelectionController::ActivateSelection() {
421 DCHECK_NE(INSERTION_ACTIVE
, active_status_
);
423 if (!start_selection_handle_
) {
424 start_selection_handle_
.reset(new TouchHandle(this, start_orientation_
));
426 start_selection_handle_
->SetEnabled(true);
427 start_selection_handle_
->SetOrientation(start_orientation_
);
430 if (!end_selection_handle_
) {
431 end_selection_handle_
.reset(new TouchHandle(this, end_orientation_
));
433 end_selection_handle_
->SetEnabled(true);
434 end_selection_handle_
->SetOrientation(end_orientation_
);
437 // As a long press received while a selection is already active may trigger
438 // an entirely new selection, notify the client but avoid sending an
439 // intervening SELECTION_CLEARED update to avoid unnecessary state changes.
440 if (active_status_
== INACTIVE
||
441 response_pending_input_event_
== LONG_PRESS
) {
442 if (active_status_
== SELECTION_ACTIVE
) {
443 // The active selection session finishes with the start of the new one.
446 active_status_
= SELECTION_ACTIVE
;
447 selection_handle_dragged_
= false;
448 selection_start_time_
= base::TimeTicks::Now();
449 response_pending_input_event_
= INPUT_EVENT_TYPE_NONE
;
450 client_
->OnSelectionEvent(SELECTION_SHOWN
);
454 void TouchSelectionController::DeactivateSelection() {
455 if (active_status_
!= SELECTION_ACTIVE
)
457 DCHECK(start_selection_handle_
);
458 DCHECK(end_selection_handle_
);
460 start_selection_handle_
->SetEnabled(false);
461 end_selection_handle_
->SetEnabled(false);
462 active_status_
= INACTIVE
;
463 client_
->OnSelectionEvent(SELECTION_CLEARED
);
466 void TouchSelectionController::ResetCachedValuesIfInactive() {
467 if (active_status_
!= INACTIVE
)
469 start_
= SelectionBound();
470 end_
= SelectionBound();
471 start_orientation_
= TouchHandleOrientation::UNDEFINED
;
472 end_orientation_
= TouchHandleOrientation::UNDEFINED
;
475 gfx::Vector2dF
TouchSelectionController::GetStartLineOffset() const {
476 return ComputeLineOffsetFromBottom(start_
);
479 gfx::Vector2dF
TouchSelectionController::GetEndLineOffset() const {
480 return ComputeLineOffsetFromBottom(end_
);
483 bool TouchSelectionController::GetStartVisible() const {
484 return start_
.visible() && !temporarily_hidden_
;
487 bool TouchSelectionController::GetEndVisible() const {
488 return end_
.visible() && !temporarily_hidden_
;
491 TouchHandle::AnimationStyle
TouchSelectionController::GetAnimationStyle(
492 bool was_active
) const {
493 return was_active
&& client_
->SupportsAnimation()
494 ? TouchHandle::ANIMATION_SMOOTH
495 : TouchHandle::ANIMATION_NONE
;
498 void TouchSelectionController::LogSelectionEnd() {
499 // TODO(mfomitchev): Once we are able to tell the difference between
500 // 'successful' and 'unsuccessful' selections - log
501 // Event.TouchSelection.Duration instead and get rid of
502 // Event.TouchSelectionD.WasDraggeduration.
503 if (selection_handle_dragged_
) {
504 base::TimeDelta duration
= base::TimeTicks::Now() - selection_start_time_
;
505 UMA_HISTOGRAM_CUSTOM_TIMES("Event.TouchSelection.WasDraggedDuration",
507 base::TimeDelta::FromMilliseconds(500),
508 base::TimeDelta::FromSeconds(60),