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"
13 TouchHandleOrientation
ToTouchHandleOrientation(SelectionBound::Type type
) {
15 case SelectionBound::LEFT
:
16 return TOUCH_HANDLE_LEFT
;
17 case SelectionBound::RIGHT
:
18 return TOUCH_HANDLE_RIGHT
;
19 case SelectionBound::CENTER
:
20 return TOUCH_HANDLE_CENTER
;
21 case SelectionBound::EMPTY
:
22 return TOUCH_HANDLE_ORIENTATION_UNDEFINED
;
24 NOTREACHED() << "Invalid selection bound type: " << type
;
25 return TOUCH_HANDLE_ORIENTATION_UNDEFINED
;
30 TouchSelectionController::TouchSelectionController(
31 TouchSelectionControllerClient
* client
,
32 base::TimeDelta tap_timeout
,
35 tap_timeout_(tap_timeout
),
37 response_pending_input_event_(INPUT_EVENT_TYPE_NONE
),
38 start_orientation_(TOUCH_HANDLE_ORIENTATION_UNDEFINED
),
39 end_orientation_(TOUCH_HANDLE_ORIENTATION_UNDEFINED
),
40 is_insertion_active_(false),
41 activate_insertion_automatically_(false),
42 is_selection_active_(false),
43 activate_selection_automatically_(false),
44 selection_empty_(false),
45 selection_editable_(false),
46 temporarily_hidden_(false) {
48 HideAndDisallowShowingAutomatically();
51 TouchSelectionController::~TouchSelectionController() {
54 void TouchSelectionController::OnSelectionBoundsChanged(
55 const SelectionBound
& start
,
56 const SelectionBound
& end
) {
57 if (start
== start_
&& end_
== end
)
62 start_orientation_
= ToTouchHandleOrientation(start_
.type());
63 end_orientation_
= ToTouchHandleOrientation(end_
.type());
65 if (!activate_selection_automatically_
&&
66 !activate_insertion_automatically_
) {
67 DCHECK_EQ(INPUT_EVENT_TYPE_NONE
, response_pending_input_event_
);
71 // Ensure that |response_pending_input_event_| is cleared after the method
72 // completes, while also making its current value available for the duration
74 InputEventType causal_input_event
= response_pending_input_event_
;
75 response_pending_input_event_
= INPUT_EVENT_TYPE_NONE
;
76 base::AutoReset
<InputEventType
> auto_reset_response_pending_input_event(
77 &response_pending_input_event_
, causal_input_event
);
79 const bool is_selection_dragging
=
80 is_selection_active_
&& (start_selection_handle_
->is_dragging() ||
81 end_selection_handle_
->is_dragging());
83 // It's possible that the bounds temporarily overlap while a selection handle
84 // is being dragged, incorrectly reporting a CENTER orientation.
85 // TODO(jdduke): This safeguard is racy, as it's possible the delayed response
86 // from handle positioning occurs *after* the handle dragging has ceased.
87 // Instead, prevent selection -> insertion transitions without an intervening
88 // action or selection clearing of some sort, crbug.com/392696.
89 if (is_selection_dragging
) {
90 if (start_orientation_
== TOUCH_HANDLE_CENTER
)
91 start_orientation_
= start_selection_handle_
->orientation();
92 if (end_orientation_
== TOUCH_HANDLE_CENTER
)
93 end_orientation_
= end_selection_handle_
->orientation();
96 if (GetStartPosition() != GetEndPosition() ||
97 (is_selection_dragging
&&
98 start_orientation_
!= TOUCH_HANDLE_ORIENTATION_UNDEFINED
&&
99 end_orientation_
!= TOUCH_HANDLE_ORIENTATION_UNDEFINED
)) {
100 OnSelectionChanged();
104 if (start_orientation_
== TOUCH_HANDLE_CENTER
&& selection_editable_
) {
105 OnInsertionChanged();
109 HideAndDisallowShowingAutomatically();
112 bool TouchSelectionController::WillHandleTouchEvent(const MotionEvent
& event
) {
113 if (is_insertion_active_
) {
114 DCHECK(insertion_handle_
);
115 return insertion_handle_
->WillHandleTouchEvent(event
);
118 if (is_selection_active_
) {
119 DCHECK(start_selection_handle_
);
120 DCHECK(end_selection_handle_
);
121 if (start_selection_handle_
->is_dragging())
122 return start_selection_handle_
->WillHandleTouchEvent(event
);
124 if (end_selection_handle_
->is_dragging())
125 return end_selection_handle_
->WillHandleTouchEvent(event
);
127 const gfx::PointF
event_pos(event
.GetX(), event
.GetY());
128 if ((event_pos
- GetStartPosition()).LengthSquared() <=
129 (event_pos
- GetEndPosition()).LengthSquared())
130 return start_selection_handle_
->WillHandleTouchEvent(event
);
132 return end_selection_handle_
->WillHandleTouchEvent(event
);
138 void TouchSelectionController::OnLongPressEvent() {
139 response_pending_input_event_
= LONG_PRESS
;
140 ShowSelectionHandlesAutomatically();
141 ShowInsertionHandleAutomatically();
142 ResetCachedValuesIfInactive();
145 void TouchSelectionController::AllowShowingFromCurrentSelection() {
146 if (is_selection_active_
|| is_insertion_active_
)
149 activate_selection_automatically_
= true;
150 activate_insertion_automatically_
= true;
151 if (GetStartPosition() != GetEndPosition())
152 OnSelectionChanged();
153 else if (start_orientation_
== TOUCH_HANDLE_CENTER
&& selection_editable_
)
154 OnInsertionChanged();
157 void TouchSelectionController::OnTapEvent() {
158 response_pending_input_event_
= TAP
;
159 ShowInsertionHandleAutomatically();
160 if (selection_empty_
)
161 DeactivateInsertion();
162 ResetCachedValuesIfInactive();
165 void TouchSelectionController::HideAndDisallowShowingAutomatically() {
166 response_pending_input_event_
= INPUT_EVENT_TYPE_NONE
;
167 DeactivateInsertion();
168 DeactivateSelection();
169 activate_insertion_automatically_
= false;
170 activate_selection_automatically_
= false;
173 void TouchSelectionController::SetTemporarilyHidden(bool hidden
) {
174 if (temporarily_hidden_
== hidden
)
176 temporarily_hidden_
= hidden
;
178 TouchHandle::AnimationStyle animation_style
= GetAnimationStyle(true);
179 if (is_selection_active_
) {
180 start_selection_handle_
->SetVisible(GetStartVisible(), animation_style
);
181 end_selection_handle_
->SetVisible(GetEndVisible(), animation_style
);
183 if (is_insertion_active_
)
184 insertion_handle_
->SetVisible(GetStartVisible(), animation_style
);
187 void TouchSelectionController::OnSelectionEditable(bool editable
) {
188 if (selection_editable_
== editable
)
190 selection_editable_
= editable
;
191 ResetCachedValuesIfInactive();
192 if (!selection_editable_
)
193 DeactivateInsertion();
196 void TouchSelectionController::OnSelectionEmpty(bool empty
) {
197 if (selection_empty_
== empty
)
199 selection_empty_
= empty
;
200 ResetCachedValuesIfInactive();
203 bool TouchSelectionController::Animate(base::TimeTicks frame_time
) {
204 if (is_insertion_active_
)
205 return insertion_handle_
->Animate(frame_time
);
207 if (is_selection_active_
) {
208 bool needs_animate
= start_selection_handle_
->Animate(frame_time
);
209 needs_animate
|= end_selection_handle_
->Animate(frame_time
);
210 return needs_animate
;
216 void TouchSelectionController::OnHandleDragBegin(const TouchHandle
& handle
) {
217 if (&handle
== insertion_handle_
.get()) {
218 client_
->OnSelectionEvent(INSERTION_DRAG_STARTED
, handle
.position());
222 gfx::PointF base
, extent
;
223 if (&handle
== start_selection_handle_
.get()) {
224 base
= end_selection_handle_
->position() + GetEndLineOffset();
225 extent
= start_selection_handle_
->position() + GetStartLineOffset();
227 base
= start_selection_handle_
->position() + GetStartLineOffset();
228 extent
= end_selection_handle_
->position() + GetEndLineOffset();
231 // When moving the handle we want to move only the extent point. Before doing
232 // so we must make sure that the base point is set correctly.
233 client_
->SelectBetweenCoordinates(base
, extent
);
235 client_
->OnSelectionEvent(SELECTION_DRAG_STARTED
, handle
.position());
238 void TouchSelectionController::OnHandleDragUpdate(const TouchHandle
& handle
,
239 const gfx::PointF
& position
) {
240 // As the position corresponds to the bottom left point of the selection
241 // bound, offset it by half the corresponding line height.
242 gfx::Vector2dF line_offset
= &handle
== end_selection_handle_
.get()
243 ? GetStartLineOffset()
244 : GetEndLineOffset();
245 gfx::PointF line_position
= position
+ line_offset
;
246 if (&handle
== insertion_handle_
.get()) {
247 client_
->MoveCaret(line_position
);
249 client_
->MoveRangeSelectionExtent(line_position
);
253 void TouchSelectionController::OnHandleDragEnd(const TouchHandle
& handle
) {
254 if (&handle
!= insertion_handle_
.get())
255 client_
->OnSelectionEvent(SELECTION_DRAG_STOPPED
, handle
.position());
258 void TouchSelectionController::OnHandleTapped(const TouchHandle
& handle
) {
259 if (insertion_handle_
&& &handle
== insertion_handle_
.get())
260 client_
->OnSelectionEvent(INSERTION_TAPPED
, handle
.position());
263 void TouchSelectionController::SetNeedsAnimate() {
264 client_
->SetNeedsAnimate();
267 scoped_ptr
<TouchHandleDrawable
> TouchSelectionController::CreateDrawable() {
268 return client_
->CreateDrawable();
271 base::TimeDelta
TouchSelectionController::GetTapTimeout() const {
275 float TouchSelectionController::GetTapSlop() const {
279 void TouchSelectionController::ShowInsertionHandleAutomatically() {
280 if (activate_insertion_automatically_
)
282 activate_insertion_automatically_
= true;
283 ResetCachedValuesIfInactive();
286 void TouchSelectionController::ShowSelectionHandlesAutomatically() {
287 if (activate_selection_automatically_
)
289 activate_selection_automatically_
= true;
290 ResetCachedValuesIfInactive();
293 void TouchSelectionController::OnInsertionChanged() {
294 DeactivateSelection();
296 if (response_pending_input_event_
== TAP
&& selection_empty_
) {
297 HideAndDisallowShowingAutomatically();
301 if (!activate_insertion_automatically_
)
304 const bool was_active
= is_insertion_active_
;
305 const gfx::PointF position
= GetStartPosition();
306 if (!is_insertion_active_
)
309 client_
->OnSelectionEvent(INSERTION_MOVED
, position
);
311 insertion_handle_
->SetVisible(GetStartVisible(),
312 GetAnimationStyle(was_active
));
313 insertion_handle_
->SetPosition(position
);
316 void TouchSelectionController::OnSelectionChanged() {
317 DeactivateInsertion();
319 if (!activate_selection_automatically_
)
322 const bool was_active
= is_selection_active_
;
325 const TouchHandle::AnimationStyle animation
= GetAnimationStyle(was_active
);
326 start_selection_handle_
->SetVisible(GetStartVisible(), animation
);
327 end_selection_handle_
->SetVisible(GetEndVisible(), animation
);
329 start_selection_handle_
->SetPosition(GetStartPosition());
330 end_selection_handle_
->SetPosition(GetEndPosition());
333 void TouchSelectionController::ActivateInsertion() {
334 DCHECK(!is_selection_active_
);
336 if (!insertion_handle_
)
337 insertion_handle_
.reset(new TouchHandle(this, TOUCH_HANDLE_CENTER
));
339 if (!is_insertion_active_
) {
340 is_insertion_active_
= true;
341 insertion_handle_
->SetEnabled(true);
342 client_
->OnSelectionEvent(INSERTION_SHOWN
, GetStartPosition());
346 void TouchSelectionController::DeactivateInsertion() {
347 if (!is_insertion_active_
)
349 DCHECK(insertion_handle_
);
350 is_insertion_active_
= false;
351 insertion_handle_
->SetEnabled(false);
352 client_
->OnSelectionEvent(INSERTION_CLEARED
, gfx::PointF());
355 void TouchSelectionController::ActivateSelection() {
356 DCHECK(!is_insertion_active_
);
358 if (!start_selection_handle_
) {
359 start_selection_handle_
.reset(new TouchHandle(this, start_orientation_
));
361 start_selection_handle_
->SetEnabled(true);
362 start_selection_handle_
->SetOrientation(start_orientation_
);
365 if (!end_selection_handle_
) {
366 end_selection_handle_
.reset(new TouchHandle(this, end_orientation_
));
368 end_selection_handle_
->SetEnabled(true);
369 end_selection_handle_
->SetOrientation(end_orientation_
);
372 // As a long press received while a selection is already active may trigger
373 // an entirely new selection, notify the client but avoid sending an
374 // intervening SELECTION_CLEARED update to avoid unnecessary state changes.
375 if (!is_selection_active_
|| response_pending_input_event_
== LONG_PRESS
) {
376 is_selection_active_
= true;
377 response_pending_input_event_
= INPUT_EVENT_TYPE_NONE
;
378 client_
->OnSelectionEvent(SELECTION_SHOWN
, GetStartPosition());
382 void TouchSelectionController::DeactivateSelection() {
383 if (!is_selection_active_
)
385 DCHECK(start_selection_handle_
);
386 DCHECK(end_selection_handle_
);
387 start_selection_handle_
->SetEnabled(false);
388 end_selection_handle_
->SetEnabled(false);
389 is_selection_active_
= false;
390 client_
->OnSelectionEvent(SELECTION_CLEARED
, gfx::PointF());
393 void TouchSelectionController::ResetCachedValuesIfInactive() {
394 if (is_selection_active_
|| is_insertion_active_
)
396 start_
= SelectionBound();
397 end_
= SelectionBound();
398 start_orientation_
= TOUCH_HANDLE_ORIENTATION_UNDEFINED
;
399 end_orientation_
= TOUCH_HANDLE_ORIENTATION_UNDEFINED
;
402 const gfx::PointF
& TouchSelectionController::GetStartPosition() const {
403 return start_
.edge_bottom();
406 const gfx::PointF
& TouchSelectionController::GetEndPosition() const {
407 return end_
.edge_bottom();
410 gfx::Vector2dF
TouchSelectionController::GetStartLineOffset() const {
411 return gfx::ScaleVector2d(start_
.edge_top() - start_
.edge_bottom(), 0.5f
);
414 gfx::Vector2dF
TouchSelectionController::GetEndLineOffset() const {
415 return gfx::ScaleVector2d(end_
.edge_top() - end_
.edge_bottom(), 0.5f
);
418 bool TouchSelectionController::GetStartVisible() const {
419 return start_
.visible() && !temporarily_hidden_
;
422 bool TouchSelectionController::GetEndVisible() const {
423 return end_
.visible() && !temporarily_hidden_
;
426 TouchHandle::AnimationStyle
TouchSelectionController::GetAnimationStyle(
427 bool was_active
) const {
428 return was_active
&& client_
->SupportsAnimation()
429 ? TouchHandle::ANIMATION_SMOOTH
430 : TouchHandle::ANIMATION_NONE
;