Scroll in Android HTML Element accessibility navigation
[chromium-blink-merge.git] / ui / touch_selection / touch_selection_controller.cc
blob1c42ea0f522c402b75cdc2464580b368046a9811
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"
11 namespace ui {
12 namespace {
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);
23 return line_offset;
26 TouchHandleOrientation ToTouchHandleOrientation(SelectionBound::Type type) {
27 switch (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;
41 } // namespace
43 TouchSelectionController::Config::Config()
44 : tap_timeout(base::TimeDelta::FromMilliseconds(100)),
45 tap_slop(8),
46 enable_longpress_drag_selection(false),
47 show_on_tap_for_empty_editable(false) {
50 TouchSelectionController::Config::~Config() {
53 TouchSelectionController::TouchSelectionController(
54 TouchSelectionControllerClient* client,
55 const Config& config)
56 : client_(client),
57 config_(config),
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) {
71 DCHECK(client_);
74 TouchSelectionController::~TouchSelectionController() {
77 void TouchSelectionController::OnSelectionBoundsChanged(
78 const SelectionBound& start,
79 const SelectionBound& end) {
80 if (!force_next_update_ && start == start_ && end_ == end)
81 return;
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);
92 start_ = start;
93 end_ = end;
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_);
102 return;
105 // Ensure that |response_pending_input_event_| is cleared after the method
106 // completes, while also making its current value available for the duration
107 // of the call.
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();
135 return;
138 if (start_orientation_ == TouchHandleOrientation::CENTER &&
139 selection_editable_) {
140 OnInsertionChanged();
141 return;
144 HideAndDisallowShowingAutomatically();
147 bool TouchSelectionController::WillHandleTouchEvent(const MotionEvent& event) {
148 if (config_.enable_longpress_drag_selection &&
149 longpress_drag_selector_.WillHandleTouchEvent(event)) {
150 return true;
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);
175 return false;
178 bool TouchSelectionController::WillHandleTapEvent(const gfx::PointF& location) {
179 if (WillHandleTapOrLongPress(location))
180 return true;
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();
189 return false;
192 bool TouchSelectionController::WillHandleLongPressEvent(
193 base::TimeTicks event_time,
194 const gfx::PointF& location) {
195 if (WillHandleTapOrLongPress(location))
196 return true;
198 longpress_drag_selector_.OnLongPressEvent(event_time, location);
199 response_pending_input_event_ = LONG_PRESS;
200 ShowSelectionHandlesAutomatically();
201 ShowInsertionHandleAutomatically();
202 ForceNextUpdateIfInactive();
203 return false;
206 void TouchSelectionController::AllowShowingFromCurrentSelection() {
207 if (active_status_ != INACTIVE)
208 return;
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)
230 return;
231 temporarily_hidden_ = hidden;
232 RefreshHandleVisibility();
235 void TouchSelectionController::OnSelectionEditable(bool editable) {
236 if (selection_editable_ == editable)
237 return;
238 selection_editable_ = editable;
239 ForceNextUpdateIfInactive();
240 if (!selection_editable_)
241 DeactivateInsertion();
244 void TouchSelectionController::OnSelectionEmpty(bool empty) {
245 if (selection_empty_ == empty)
246 return;
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;
261 return false;
264 gfx::RectF TouchSelectionController::GetRectBetweenBounds() const {
265 // Short-circuit for efficiency.
266 if (active_status_ == INACTIVE)
267 return gfx::RectF();
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();
284 return gfx::RectF();
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();
292 return gfx::RectF();
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;
310 return;
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;
319 } else {
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);
350 else
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);
358 else
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_)
401 return;
402 activate_insertion_automatically_ = true;
403 ForceNextUpdateIfInactive();
406 void TouchSelectionController::ShowSelectionHandlesAutomatically() {
407 if (activate_selection_automatically_)
408 return;
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
419 // real selection.
420 if (active_status_ == INACTIVE &&
421 GetStartPosition() != GetEndPosition() &&
422 RectFBetweenSelectionBounds(start_, end_).Contains(location)) {
423 AllowShowingFromCurrentSelection();
424 return true;
426 return false;
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();
435 return;
438 if (!activate_insertion_automatically_)
439 return;
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_)
455 return;
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);
480 return true;
482 return false;
485 void TouchSelectionController::DeactivateInsertion() {
486 if (active_status_ != INSERTION_ACTIVE)
487 return;
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_));
499 } else {
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_));
506 } else {
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
514 // changes.
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.
519 LogSelectionEnd();
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();
526 return true;
528 return false;
531 void TouchSelectionController::DeactivateSelection() {
532 if (active_status_ != SELECTION_ACTIVE)
533 return;
534 DCHECK(start_selection_handle_);
535 DCHECK(end_selection_handle_);
536 LogSelectionEnd();
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())
575 return false;
577 return !temporarily_hidden_ && !longpress_drag_selector_.IsActive();
580 bool TouchSelectionController::GetEndVisible() const {
581 if (!end_.visible())
582 return false;
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",
602 duration,
603 base::TimeDelta::FromMilliseconds(500),
604 base::TimeDelta::FromSeconds(60),
605 60);
609 } // namespace ui