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. 4 5 #include "content/browser/renderer_host/input/touch_selection_controller.h" 6 7 #include "base/auto_reset.h" 8 #include "base/logging.h" 9 #include "third_party/WebKit/public/web/WebInputEvent.h" 10 11 namespace content { 12 namespace { 13 14 TouchHandleOrientation ToTouchHandleOrientation(cc::SelectionBoundType type) { 15 switch (type) { 16 case cc::SELECTION_BOUND_LEFT: 17 return TOUCH_HANDLE_LEFT; 18 case cc::SELECTION_BOUND_RIGHT: 19 return TOUCH_HANDLE_RIGHT; 20 case cc::SELECTION_BOUND_CENTER: 21 return TOUCH_HANDLE_CENTER; 22 case cc::SELECTION_BOUND_EMPTY: 23 return TOUCH_HANDLE_ORIENTATION_UNDEFINED; 24 } 25 NOTREACHED() << "Invalid selection bound type: " << type; 26 return TOUCH_HANDLE_ORIENTATION_UNDEFINED; 27 } 28 29 } // namespace 30 31 TouchSelectionController::TouchSelectionController( 32 TouchSelectionControllerClient* client, 33 base::TimeDelta tap_timeout, 34 float tap_slop) 35 : client_(client), 36 tap_timeout_(tap_timeout), 37 tap_slop_(tap_slop), 38 response_pending_input_event_(INPUT_EVENT_TYPE_NONE), 39 start_orientation_(TOUCH_HANDLE_ORIENTATION_UNDEFINED), 40 end_orientation_(TOUCH_HANDLE_ORIENTATION_UNDEFINED), 41 is_insertion_active_(false), 42 activate_insertion_automatically_(false), 43 is_selection_active_(false), 44 activate_selection_automatically_(false), 45 selection_empty_(false), 46 selection_editable_(false), 47 temporarily_hidden_(false) { 48 DCHECK(client_); 49 HideAndDisallowShowingAutomatically(); 50 } 51 52 TouchSelectionController::~TouchSelectionController() { 53 } 54 55 void TouchSelectionController::OnSelectionBoundsChanged( 56 const cc::ViewportSelectionBound& start, 57 const cc::ViewportSelectionBound& end) { 58 if (!activate_selection_automatically_ && 59 !activate_insertion_automatically_) { 60 DCHECK_EQ(INPUT_EVENT_TYPE_NONE, response_pending_input_event_); 61 return; 62 } 63 64 if (start == start_ && end_ == end) 65 return; 66 67 start_ = start; 68 end_ = end; 69 start_orientation_ = ToTouchHandleOrientation(start_.type); 70 end_orientation_ = ToTouchHandleOrientation(end_.type); 71 72 // Ensure that |response_pending_input_event_| is cleared after the method 73 // completes, while also making its current value available for the duration 74 // of the call. 75 InputEventType causal_input_event = response_pending_input_event_; 76 response_pending_input_event_ = INPUT_EVENT_TYPE_NONE; 77 base::AutoReset<InputEventType> auto_reset_response_pending_input_event( 78 &response_pending_input_event_, causal_input_event); 79 80 const bool is_selection_dragging = 81 is_selection_active_ && (start_selection_handle_->is_dragging() || 82 end_selection_handle_->is_dragging()); 83 84 // It's possible that the bounds temporarily overlap while a selection handle 85 // is being dragged, incorrectly reporting a CENTER orientation. 86 // TODO(jdduke): This safeguard is racy, as it's possible the delayed response 87 // from handle positioning occurs *after* the handle dragging has ceased. 88 // Instead, prevent selection -> insertion transitions without an intervening 89 // action or selection clearing of some sort, crbug.com/392696. 90 if (is_selection_dragging) { 91 if (start_orientation_ == TOUCH_HANDLE_CENTER) 92 start_orientation_ = start_selection_handle_->orientation(); 93 if (end_orientation_ == TOUCH_HANDLE_CENTER) 94 end_orientation_ = end_selection_handle_->orientation(); 95 } 96 97 if (GetStartPosition() != GetEndPosition() || 98 (is_selection_dragging && 99 start_orientation_ != TOUCH_HANDLE_ORIENTATION_UNDEFINED && 100 end_orientation_ != TOUCH_HANDLE_ORIENTATION_UNDEFINED)) { 101 OnSelectionChanged(); 102 return; 103 } 104 105 if (start_orientation_ == TOUCH_HANDLE_CENTER && selection_editable_) { 106 OnInsertionChanged(); 107 return; 108 } 109 110 HideAndDisallowShowingAutomatically(); 111 } 112 113 bool TouchSelectionController::WillHandleTouchEvent( 114 const ui::MotionEvent& event) { 115 if (is_insertion_active_) { 116 DCHECK(insertion_handle_); 117 return insertion_handle_->WillHandleTouchEvent(event); 118 } 119 120 if (is_selection_active_) { 121 DCHECK(start_selection_handle_); 122 DCHECK(end_selection_handle_); 123 if (start_selection_handle_->is_dragging()) 124 return start_selection_handle_->WillHandleTouchEvent(event); 125 126 if (end_selection_handle_->is_dragging()) 127 return end_selection_handle_->WillHandleTouchEvent(event); 128 129 const gfx::PointF event_pos(event.GetX(), event.GetY()); 130 if ((event_pos - GetStartPosition()).LengthSquared() <= 131 (event_pos - GetEndPosition()).LengthSquared()) 132 return start_selection_handle_->WillHandleTouchEvent(event); 133 else 134 return end_selection_handle_->WillHandleTouchEvent(event); 135 } 136 137 return false; 138 } 139 140 void TouchSelectionController::OnLongPressEvent() { 141 response_pending_input_event_ = LONG_PRESS; 142 ShowSelectionHandlesAutomatically(); 143 ShowInsertionHandleAutomatically(); 144 ResetCachedValuesIfInactive(); 145 } 146 147 void TouchSelectionController::OnTapEvent() { 148 response_pending_input_event_ = TAP; 149 ShowInsertionHandleAutomatically(); 150 if (selection_empty_) 151 DeactivateInsertion(); 152 ResetCachedValuesIfInactive(); 153 } 154 155 void TouchSelectionController::HideAndDisallowShowingAutomatically() { 156 response_pending_input_event_ = INPUT_EVENT_TYPE_NONE; 157 DeactivateInsertion(); 158 DeactivateSelection(); 159 activate_insertion_automatically_ = false; 160 activate_selection_automatically_ = false; 161 } 162 163 void TouchSelectionController::SetTemporarilyHidden(bool hidden) { 164 if (temporarily_hidden_ == hidden) 165 return; 166 temporarily_hidden_ = hidden; 167 168 TouchHandle::AnimationStyle animation_style = GetAnimationStyle(true); 169 if (is_selection_active_) { 170 start_selection_handle_->SetVisible(GetStartVisible(), animation_style); 171 end_selection_handle_->SetVisible(GetEndVisible(), animation_style); 172 } 173 if (is_insertion_active_) 174 insertion_handle_->SetVisible(GetStartVisible(), animation_style); 175 } 176 177 void TouchSelectionController::OnSelectionEditable(bool editable) { 178 if (selection_editable_ == editable) 179 return; 180 selection_editable_ = editable; 181 ResetCachedValuesIfInactive(); 182 if (!selection_editable_) 183 DeactivateInsertion(); 184 } 185 186 void TouchSelectionController::OnSelectionEmpty(bool empty) { 187 if (selection_empty_ == empty) 188 return; 189 selection_empty_ = empty; 190 ResetCachedValuesIfInactive(); 191 } 192 193 bool TouchSelectionController::Animate(base::TimeTicks frame_time) { 194 if (is_insertion_active_) 195 return insertion_handle_->Animate(frame_time); 196 197 if (is_selection_active_) { 198 bool needs_animate = start_selection_handle_->Animate(frame_time); 199 needs_animate |= end_selection_handle_->Animate(frame_time); 200 return needs_animate; 201 } 202 203 return false; 204 } 205 206 void TouchSelectionController::OnHandleDragBegin(const TouchHandle& handle) { 207 if (&handle == insertion_handle_.get()) { 208 client_->OnSelectionEvent(INSERTION_DRAG_STARTED, handle.position()); 209 return; 210 } 211 212 if (&handle == start_selection_handle_.get()) { 213 fixed_handle_position_ = 214 end_selection_handle_->position() + GetEndLineOffset(); 215 } else { 216 fixed_handle_position_ = 217 start_selection_handle_->position() + GetStartLineOffset(); 218 } 219 client_->OnSelectionEvent(SELECTION_DRAG_STARTED, handle.position()); 220 } 221 222 void TouchSelectionController::OnHandleDragUpdate(const TouchHandle& handle, 223 const gfx::PointF& position) { 224 // As the position corresponds to the bottom left point of the selection 225 // bound, offset it by half the corresponding line height. 226 gfx::Vector2dF line_offset = &handle == end_selection_handle_.get() 227 ? GetStartLineOffset() 228 : GetEndLineOffset(); 229 gfx::PointF line_position = position + line_offset; 230 if (&handle == insertion_handle_.get()) { 231 client_->MoveCaret(line_position); 232 } else { 233 client_->SelectBetweenCoordinates(fixed_handle_position_, line_position); 234 } 235 } 236 237 void TouchSelectionController::OnHandleDragEnd(const TouchHandle& handle) { 238 if (&handle != insertion_handle_.get()) 239 client_->OnSelectionEvent(SELECTION_DRAG_STOPPED, handle.position()); 240 } 241 242 void TouchSelectionController::OnHandleTapped(const TouchHandle& handle) { 243 if (insertion_handle_ && &handle == insertion_handle_.get()) 244 client_->OnSelectionEvent(INSERTION_TAPPED, handle.position()); 245 } 246 247 void TouchSelectionController::SetNeedsAnimate() { 248 client_->SetNeedsAnimate(); 249 } 250 251 scoped_ptr<TouchHandleDrawable> TouchSelectionController::CreateDrawable() { 252 return client_->CreateDrawable(); 253 } 254 255 base::TimeDelta TouchSelectionController::GetTapTimeout() const { 256 return tap_timeout_; 257 } 258 259 float TouchSelectionController::GetTapSlop() const { 260 return tap_slop_; 261 } 262 263 void TouchSelectionController::ShowInsertionHandleAutomatically() { 264 if (activate_insertion_automatically_) 265 return; 266 activate_insertion_automatically_ = true; 267 ResetCachedValuesIfInactive(); 268 } 269 270 void TouchSelectionController::ShowSelectionHandlesAutomatically() { 271 if (activate_selection_automatically_) 272 return; 273 activate_selection_automatically_ = true; 274 ResetCachedValuesIfInactive(); 275 } 276 277 void TouchSelectionController::OnInsertionChanged() { 278 DeactivateSelection(); 279 280 if (response_pending_input_event_ == TAP && selection_empty_) { 281 HideAndDisallowShowingAutomatically(); 282 return; 283 } 284 285 if (!activate_insertion_automatically_) 286 return; 287 288 const bool was_active = is_insertion_active_; 289 const gfx::PointF position = GetStartPosition(); 290 if (!is_insertion_active_) 291 ActivateInsertion(); 292 else 293 client_->OnSelectionEvent(INSERTION_MOVED, position); 294 295 insertion_handle_->SetVisible(GetStartVisible(), 296 GetAnimationStyle(was_active)); 297 insertion_handle_->SetPosition(position); 298 } 299 300 void TouchSelectionController::OnSelectionChanged() { 301 DeactivateInsertion(); 302 303 if (!activate_selection_automatically_) 304 return; 305 306 const bool was_active = is_selection_active_; 307 ActivateSelection(); 308 309 const TouchHandle::AnimationStyle animation = GetAnimationStyle(was_active); 310 start_selection_handle_->SetVisible(GetStartVisible(), animation); 311 end_selection_handle_->SetVisible(GetEndVisible(), animation); 312 313 start_selection_handle_->SetPosition(GetStartPosition()); 314 end_selection_handle_->SetPosition(GetEndPosition()); 315 } 316 317 void TouchSelectionController::ActivateInsertion() { 318 DCHECK(!is_selection_active_); 319 320 if (!insertion_handle_) 321 insertion_handle_.reset(new TouchHandle(this, TOUCH_HANDLE_CENTER)); 322 323 if (!is_insertion_active_) { 324 is_insertion_active_ = true; 325 insertion_handle_->SetEnabled(true); 326 client_->OnSelectionEvent(INSERTION_SHOWN, GetStartPosition()); 327 } 328 } 329 330 void TouchSelectionController::DeactivateInsertion() { 331 if (!is_insertion_active_) 332 return; 333 DCHECK(insertion_handle_); 334 is_insertion_active_ = false; 335 insertion_handle_->SetEnabled(false); 336 client_->OnSelectionEvent(INSERTION_CLEARED, gfx::PointF()); 337 } 338 339 void TouchSelectionController::ActivateSelection() { 340 DCHECK(!is_insertion_active_); 341 342 if (!start_selection_handle_) { 343 start_selection_handle_.reset(new TouchHandle(this, start_orientation_)); 344 } else { 345 start_selection_handle_->SetEnabled(true); 346 start_selection_handle_->SetOrientation(start_orientation_); 347 } 348 349 if (!end_selection_handle_) { 350 end_selection_handle_.reset(new TouchHandle(this, end_orientation_)); 351 } else { 352 end_selection_handle_->SetEnabled(true); 353 end_selection_handle_->SetOrientation(end_orientation_); 354 } 355 356 // As a long press received while a selection is already active may trigger 357 // an entirely new selection, notify the client but avoid sending an 358 // intervening SELECTION_CLEARED update to avoid unnecessary state changes. 359 if (!is_selection_active_ || response_pending_input_event_ == LONG_PRESS) { 360 is_selection_active_ = true; 361 response_pending_input_event_ = INPUT_EVENT_TYPE_NONE; 362 client_->OnSelectionEvent(SELECTION_SHOWN, GetStartPosition()); 363 } 364 } 365 366 void TouchSelectionController::DeactivateSelection() { 367 if (!is_selection_active_) 368 return; 369 DCHECK(start_selection_handle_); 370 DCHECK(end_selection_handle_); 371 start_selection_handle_->SetEnabled(false); 372 end_selection_handle_->SetEnabled(false); 373 is_selection_active_ = false; 374 client_->OnSelectionEvent(SELECTION_CLEARED, gfx::PointF()); 375 } 376 377 void TouchSelectionController::ResetCachedValuesIfInactive() { 378 if (is_selection_active_ || is_insertion_active_) 379 return; 380 start_ = cc::ViewportSelectionBound(); 381 end_ = cc::ViewportSelectionBound(); 382 start_orientation_ = TOUCH_HANDLE_ORIENTATION_UNDEFINED; 383 end_orientation_ = TOUCH_HANDLE_ORIENTATION_UNDEFINED; 384 } 385 386 const gfx::PointF& TouchSelectionController::GetStartPosition() const { 387 return start_.edge_bottom; 388 } 389 390 const gfx::PointF& TouchSelectionController::GetEndPosition() const { 391 return end_.edge_bottom; 392 } 393 394 gfx::Vector2dF TouchSelectionController::GetStartLineOffset() const { 395 return gfx::ScaleVector2d(start_.edge_top - start_.edge_bottom, 0.5f); 396 } 397 398 gfx::Vector2dF TouchSelectionController::GetEndLineOffset() const { 399 return gfx::ScaleVector2d(end_.edge_top - end_.edge_bottom, 0.5f); 400 } 401 402 bool TouchSelectionController::GetStartVisible() const { 403 return start_.visible && !temporarily_hidden_; 404 } 405 406 bool TouchSelectionController::GetEndVisible() const { 407 return end_.visible && !temporarily_hidden_; 408 } 409 410 TouchHandle::AnimationStyle TouchSelectionController::GetAnimationStyle( 411 bool was_active) const { 412 return was_active && client_->SupportsAnimation() 413 ? TouchHandle::ANIMATION_SMOOTH 414 : TouchHandle::ANIMATION_NONE; 415 } 416 417 } // namespace content 418