1 // Copyright (c) 2013 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 "ui/views/touchui/touch_selection_controller_impl.h" 6 7 #include "base/time/time.h" 8 #include "grit/ui_resources.h" 9 #include "grit/ui_strings.h" 10 #include "ui/base/resource/resource_bundle.h" 11 #include "ui/base/ui_base_switches_util.h" 12 #include "ui/gfx/canvas.h" 13 #include "ui/gfx/image/image.h" 14 #include "ui/gfx/path.h" 15 #include "ui/gfx/rect.h" 16 #include "ui/gfx/screen.h" 17 #include "ui/gfx/size.h" 18 #include "ui/views/corewm/shadow_types.h" 19 #include "ui/views/widget/widget.h" 20 21 namespace { 22 23 // Constants defining the visual attributes of selection handles 24 const int kSelectionHandleLineWidth = 1; 25 const SkColor kSelectionHandleLineColor = 26 SkColorSetRGB(0x42, 0x81, 0xf4); 27 28 // When a handle is dragged, the drag position reported to the client view is 29 // offset vertically to represent the cursor position. This constant specifies 30 // the offset in pixels above the "O" (see pic below). This is required because 31 // say if this is zero, that means the drag position we report is the point 32 // right above the "O" or the bottom most point of the cursor "|". In that case, 33 // a vertical movement of even one pixel will make the handle jump to the line 34 // below it. So when the user just starts dragging, the handle will jump to the 35 // next line if the user makes any vertical movement. It is correct but 36 // looks/feels weird. So we have this non-zero offset to prevent this jumping. 37 // 38 // Editing handle widget showing the difference between the position of the 39 // ET_GESTURE_SCROLL_UPDATE event and the drag position reported to the client: 40 // _____ 41 // | |<-|---- Drag position reported to client 42 // _ | O | 43 // Vertical Padding __| | <-|---- ET_GESTURE_SCROLL_UPDATE position 44 // |_ |_____|<--- Editing handle widget 45 // 46 // | | 47 // T 48 // Horizontal Padding 49 // 50 const int kSelectionHandleVerticalDragOffset = 5; 51 52 // Padding around the selection handle defining the area that will be included 53 // in the touch target to make dragging the handle easier (see pic above). 54 const int kSelectionHandleHorizPadding = 10; 55 const int kSelectionHandleVertPadding = 20; 56 57 // The minimum selection size to trigger selection controller. 58 // TODO(varunjain): Figure out if this is really required and get rid of it if 59 // it isnt. 60 const int kMinSelectionSize = 1; 61 62 const int kContextMenuTimoutMs = 200; 63 64 // Creates a widget to host SelectionHandleView. 65 views::Widget* CreateTouchSelectionPopupWidget( 66 gfx::NativeView context, 67 views::WidgetDelegate* widget_delegate) { 68 views::Widget* widget = new views::Widget; 69 views::Widget::InitParams params(views::Widget::InitParams::TYPE_TOOLTIP); 70 params.can_activate = false; 71 params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; 72 params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; 73 params.context = context; 74 params.delegate = widget_delegate; 75 widget->Init(params); 76 #if defined(USE_AURA) 77 SetShadowType(widget->GetNativeView(), views::corewm::SHADOW_TYPE_NONE); 78 #endif 79 return widget; 80 } 81 82 gfx::Image* GetHandleImage() { 83 static gfx::Image* handle_image = NULL; 84 if (!handle_image) { 85 handle_image = &ui::ResourceBundle::GetSharedInstance().GetImageNamed( 86 IDR_TEXT_SELECTION_HANDLE); 87 } 88 return handle_image; 89 } 90 91 gfx::Size GetHandleImageSize() { 92 return GetHandleImage()->Size(); 93 } 94 95 // The points may not match exactly, since the selection range computation may 96 // introduce some floating point errors. So check for a minimum size to decide 97 // whether or not there is any selection. 98 bool IsEmptySelection(const gfx::Point& p1, const gfx::Point& p2) { 99 int delta_x = p2.x() - p1.x(); 100 int delta_y = p2.y() - p1.y(); 101 return (abs(delta_x) < kMinSelectionSize && abs(delta_y) < kMinSelectionSize); 102 } 103 104 // Cannot use gfx::UnionRect since it does not work for empty rects. 105 gfx::Rect Union(const gfx::Rect& r1, const gfx::Rect& r2) { 106 int rx = std::min(r1.x(), r2.x()); 107 int ry = std::min(r1.y(), r2.y()); 108 int rr = std::max(r1.right(), r2.right()); 109 int rb = std::max(r1.bottom(), r2.bottom()); 110 111 return gfx::Rect(rx, ry, rr - rx, rb - ry); 112 } 113 114 // Convenience method to convert a |rect| from screen to the |client|'s 115 // coordinate system. 116 // Note that this is not quite correct because it does not take into account 117 // transforms such as rotation and scaling. This should be in TouchEditable. 118 // TODO(varunjain): Fix this. 119 gfx::Rect ConvertFromScreen(ui::TouchEditable* client, const gfx::Rect& rect) { 120 gfx::Point origin = rect.origin(); 121 client->ConvertPointFromScreen(&origin); 122 return gfx::Rect(origin, rect.size()); 123 } 124 125 } // namespace 126 127 namespace views { 128 129 // A View that displays the text selection handle. 130 class TouchSelectionControllerImpl::EditingHandleView 131 : public views::WidgetDelegateView { 132 public: 133 explicit EditingHandleView(TouchSelectionControllerImpl* controller, 134 gfx::NativeView context) 135 : controller_(controller), 136 drag_offset_(0), 137 draw_invisible_(false) { 138 widget_.reset(CreateTouchSelectionPopupWidget(context, this)); 139 widget_->SetContentsView(this); 140 widget_->SetAlwaysOnTop(true); 141 142 // We are owned by the TouchSelectionController. 143 set_owned_by_client(); 144 } 145 146 virtual ~EditingHandleView() { 147 } 148 149 int cursor_height() const { return selection_rect_.height(); } 150 151 // Current selection end point that this handle marks in screen coordinates. 152 const gfx::Rect selection_rect() const { return selection_rect_; } 153 154 // Overridden from views::WidgetDelegateView: 155 virtual bool WidgetHasHitTestMask() const OVERRIDE { 156 return true; 157 } 158 159 virtual void GetWidgetHitTestMask(gfx::Path* mask) const OVERRIDE { 160 gfx::Size image_size = GetHandleImageSize(); 161 mask->addRect(SkIntToScalar(0), SkIntToScalar(cursor_height()), 162 SkIntToScalar(image_size.width()) + 2 * kSelectionHandleHorizPadding, 163 SkIntToScalar(cursor_height() + image_size.height() + 164 kSelectionHandleVertPadding)); 165 } 166 167 virtual void DeleteDelegate() OVERRIDE { 168 // We are owned and deleted by TouchSelectionController. 169 } 170 171 // Overridden from views::View: 172 virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE { 173 if (draw_invisible_) 174 return; 175 gfx::Size image_size = GetHandleImageSize(); 176 int cursor_pos_x = image_size.width() / 2 - kSelectionHandleLineWidth + 177 kSelectionHandleHorizPadding; 178 179 // Draw the cursor line. 180 canvas->FillRect( 181 gfx::Rect(cursor_pos_x, 0, 182 2 * kSelectionHandleLineWidth + 1, cursor_height()), 183 kSelectionHandleLineColor); 184 185 // Draw the handle image. 186 canvas->DrawImageInt(*GetHandleImage()->ToImageSkia(), 187 kSelectionHandleHorizPadding, cursor_height()); 188 } 189 190 virtual void OnGestureEvent(ui::GestureEvent* event) OVERRIDE { 191 event->SetHandled(); 192 switch (event->type()) { 193 case ui::ET_GESTURE_SCROLL_BEGIN: 194 widget_->SetCapture(this); 195 controller_->SetDraggingHandle(this); 196 drag_offset_ = event->y() - cursor_height() - 197 kSelectionHandleVerticalDragOffset; 198 break; 199 case ui::ET_GESTURE_SCROLL_UPDATE: { 200 gfx::Point drag_pos(event->location().x(), 201 event->location().y() - drag_offset_); 202 controller_->SelectionHandleDragged(drag_pos); 203 break; 204 } 205 case ui::ET_GESTURE_SCROLL_END: 206 case ui::ET_SCROLL_FLING_START: 207 widget_->ReleaseCapture(); 208 controller_->SetDraggingHandle(NULL); 209 break; 210 default: 211 break; 212 } 213 } 214 215 virtual void SetVisible(bool visible) OVERRIDE { 216 // We simply show/hide the container widget. 217 if (visible != widget_->IsVisible()) { 218 if (visible) 219 widget_->Show(); 220 else 221 widget_->Hide(); 222 } 223 View::SetVisible(visible); 224 } 225 226 virtual gfx::Size GetPreferredSize() OVERRIDE { 227 gfx::Size image_size = GetHandleImageSize(); 228 return gfx::Size(image_size.width() + 2 * kSelectionHandleHorizPadding, 229 image_size.height() + cursor_height() + kSelectionHandleVertPadding); 230 } 231 232 bool IsWidgetVisible() const { 233 return widget_->IsVisible(); 234 } 235 236 void SetSelectionRectInScreen(const gfx::Rect& rect) { 237 gfx::Size image_size = GetHandleImageSize(); 238 selection_rect_ = rect; 239 gfx::Rect widget_bounds( 240 rect.x() - image_size.width() / 2 - kSelectionHandleHorizPadding, 241 rect.y(), 242 image_size.width() + 2 * kSelectionHandleHorizPadding, 243 rect.height() + image_size.height() + kSelectionHandleVertPadding); 244 widget_->SetBounds(widget_bounds); 245 } 246 247 gfx::Point GetScreenPosition() { 248 return widget_->GetClientAreaBoundsInScreen().origin(); 249 } 250 251 void SetDrawInvisible(bool draw_invisible) { 252 if (draw_invisible_ == draw_invisible) 253 return; 254 draw_invisible_ = draw_invisible; 255 SchedulePaint(); 256 } 257 258 private: 259 scoped_ptr<Widget> widget_; 260 TouchSelectionControllerImpl* controller_; 261 gfx::Rect selection_rect_; 262 int drag_offset_; 263 264 // If set to true, the handle will not draw anything, hence providing an empty 265 // widget. We need this because we may want to stop showing the handle while 266 // it is being dragged. Since it is being dragged, we cannot destroy the 267 // handle. 268 bool draw_invisible_; 269 270 DISALLOW_COPY_AND_ASSIGN(EditingHandleView); 271 }; 272 273 TouchSelectionControllerImpl::TouchSelectionControllerImpl( 274 ui::TouchEditable* client_view) 275 : client_view_(client_view), 276 client_widget_(NULL), 277 selection_handle_1_(new EditingHandleView(this, 278 client_view->GetNativeView())), 279 selection_handle_2_(new EditingHandleView(this, 280 client_view->GetNativeView())), 281 cursor_handle_(new EditingHandleView(this, 282 client_view->GetNativeView())), 283 context_menu_(NULL), 284 dragging_handle_(NULL) { 285 client_widget_ = Widget::GetTopLevelWidgetForNativeView( 286 client_view_->GetNativeView()); 287 if (client_widget_) 288 client_widget_->AddObserver(this); 289 } 290 291 TouchSelectionControllerImpl::~TouchSelectionControllerImpl() { 292 HideContextMenu(); 293 if (client_widget_) 294 client_widget_->RemoveObserver(this); 295 } 296 297 void TouchSelectionControllerImpl::SelectionChanged() { 298 gfx::Rect r1, r2; 299 client_view_->GetSelectionEndPoints(&r1, &r2); 300 gfx::Point screen_pos_1(r1.origin()); 301 client_view_->ConvertPointToScreen(&screen_pos_1); 302 gfx::Point screen_pos_2(r2.origin()); 303 client_view_->ConvertPointToScreen(&screen_pos_2); 304 gfx::Rect screen_rect_1(screen_pos_1, r1.size()); 305 gfx::Rect screen_rect_2(screen_pos_2, r2.size()); 306 if (screen_rect_1 == selection_end_point_1 && 307 screen_rect_2 == selection_end_point_2) 308 return; 309 310 selection_end_point_1 = screen_rect_1; 311 selection_end_point_2 = screen_rect_2; 312 313 if (client_view_->DrawsHandles()) { 314 UpdateContextMenu(r1.origin(), r2.origin()); 315 return; 316 } 317 if (dragging_handle_) { 318 // We need to reposition only the selection handle that is being dragged. 319 // The other handle stays the same. Also, the selection handle being dragged 320 // will always be at the end of selection, while the other handle will be at 321 // the start. 322 dragging_handle_->SetSelectionRectInScreen(screen_rect_2); 323 324 // Temporary fix for selection handle going outside a window. On a webpage, 325 // the page should scroll if the selection handle is dragged outside the 326 // window. That does not happen currently. So we just hide the handle for 327 // now. 328 // TODO(varunjain): Fix this: crbug.com/269003 329 dragging_handle_->SetDrawInvisible(!client_view_->GetBounds().Contains(r2)); 330 331 if (dragging_handle_ != cursor_handle_.get()) { 332 // The non-dragging-handle might have recently become visible. 333 EditingHandleView* non_dragging_handle = 334 dragging_handle_ == selection_handle_1_.get()? 335 selection_handle_2_.get() : selection_handle_1_.get(); 336 non_dragging_handle->SetSelectionRectInScreen(screen_rect_1); 337 non_dragging_handle->SetVisible(client_view_->GetBounds().Contains(r1)); 338 } 339 } else { 340 UpdateContextMenu(r1.origin(), r2.origin()); 341 342 // Check if there is any selection at all. 343 if (IsEmptySelection(screen_pos_2, screen_pos_1)) { 344 selection_handle_1_->SetVisible(false); 345 selection_handle_2_->SetVisible(false); 346 cursor_handle_->SetSelectionRectInScreen(screen_rect_1); 347 cursor_handle_->SetVisible(true); 348 return; 349 } 350 351 cursor_handle_->SetVisible(false); 352 selection_handle_1_->SetSelectionRectInScreen(screen_rect_1); 353 selection_handle_1_->SetVisible(client_view_->GetBounds().Contains(r1)); 354 355 selection_handle_2_->SetSelectionRectInScreen(screen_rect_2); 356 selection_handle_2_->SetVisible(client_view_->GetBounds().Contains(r2)); 357 } 358 } 359 360 bool TouchSelectionControllerImpl::IsHandleDragInProgress() { 361 return !!dragging_handle_; 362 } 363 364 void TouchSelectionControllerImpl::SetDraggingHandle( 365 EditingHandleView* handle) { 366 dragging_handle_ = handle; 367 if (dragging_handle_) 368 HideContextMenu(); 369 else 370 StartContextMenuTimer(); 371 } 372 373 void TouchSelectionControllerImpl::SelectionHandleDragged( 374 const gfx::Point& drag_pos) { 375 // We do not want to show the context menu while dragging. 376 HideContextMenu(); 377 378 DCHECK(dragging_handle_); 379 gfx::Point drag_pos_in_client = drag_pos; 380 ConvertPointToClientView(dragging_handle_, &drag_pos_in_client); 381 382 if (dragging_handle_ == cursor_handle_.get()) { 383 client_view_->MoveCaretTo(drag_pos_in_client); 384 return; 385 } 386 387 // Find the stationary selection handle. 388 EditingHandleView* fixed_handle = selection_handle_1_.get(); 389 if (fixed_handle == dragging_handle_) 390 fixed_handle = selection_handle_2_.get(); 391 392 // Find selection end points in client_view's coordinate system. 393 gfx::Point p2 = fixed_handle->selection_rect().origin(); 394 p2.Offset(0, fixed_handle->cursor_height() / 2); 395 client_view_->ConvertPointFromScreen(&p2); 396 397 // Instruct client_view to select the region between p1 and p2. The position 398 // of |fixed_handle| is the start and that of |dragging_handle| is the end 399 // of selection. 400 client_view_->SelectRect(p2, drag_pos_in_client); 401 } 402 403 void TouchSelectionControllerImpl::ConvertPointToClientView( 404 EditingHandleView* source, gfx::Point* point) { 405 View::ConvertPointToScreen(source, point); 406 client_view_->ConvertPointFromScreen(point); 407 } 408 409 bool TouchSelectionControllerImpl::IsCommandIdEnabled(int command_id) const { 410 return client_view_->IsCommandIdEnabled(command_id); 411 } 412 413 void TouchSelectionControllerImpl::ExecuteCommand(int command_id, 414 int event_flags) { 415 HideContextMenu(); 416 client_view_->ExecuteCommand(command_id, event_flags); 417 } 418 419 void TouchSelectionControllerImpl::OpenContextMenu() { 420 // Context menu should appear centered on top of the selected region. 421 gfx::Point anchor(context_menu_->anchor_rect().CenterPoint().x(), 422 context_menu_->anchor_rect().y()); 423 HideContextMenu(); 424 client_view_->OpenContextMenu(anchor); 425 } 426 427 void TouchSelectionControllerImpl::OnMenuClosed(TouchEditingMenuView* menu) { 428 if (menu == context_menu_) 429 context_menu_ = NULL; 430 } 431 432 void TouchSelectionControllerImpl::OnWidgetClosing(Widget* widget) { 433 DCHECK_EQ(client_widget_, widget); 434 client_widget_ = NULL; 435 } 436 437 void TouchSelectionControllerImpl::OnWidgetBoundsChanged( 438 Widget* widget, 439 const gfx::Rect& new_bounds) { 440 DCHECK_EQ(client_widget_, widget); 441 HideContextMenu(); 442 SelectionChanged(); 443 } 444 445 void TouchSelectionControllerImpl::ContextMenuTimerFired() { 446 // Get selection end points in client_view's space. 447 gfx::Rect end_rect_1_in_screen; 448 gfx::Rect end_rect_2_in_screen; 449 if (cursor_handle_->IsWidgetVisible()) { 450 end_rect_1_in_screen = cursor_handle_->selection_rect(); 451 end_rect_2_in_screen = end_rect_1_in_screen; 452 } else { 453 end_rect_1_in_screen = selection_handle_1_->selection_rect(); 454 end_rect_2_in_screen = selection_handle_2_->selection_rect(); 455 } 456 457 // Convert from screen to client. 458 gfx::Rect end_rect_1(ConvertFromScreen(client_view_, end_rect_1_in_screen)); 459 gfx::Rect end_rect_2(ConvertFromScreen(client_view_, end_rect_2_in_screen)); 460 461 // if selection is completely inside the view, we display the context menu 462 // in the middle of the end points on the top. Else, we show it above the 463 // visible handle. If no handle is visible, we do not show the menu. 464 gfx::Rect menu_anchor; 465 gfx::Rect client_bounds = client_view_->GetBounds(); 466 if (client_bounds.Contains(end_rect_1) && 467 client_bounds.Contains(end_rect_2)) 468 menu_anchor = Union(end_rect_1_in_screen,end_rect_2_in_screen); 469 else if (client_bounds.Contains(end_rect_1)) 470 menu_anchor = end_rect_1_in_screen; 471 else if (client_bounds.Contains(end_rect_2)) 472 menu_anchor = end_rect_2_in_screen; 473 else 474 return; 475 476 DCHECK(!context_menu_); 477 context_menu_ = TouchEditingMenuView::Create(this, menu_anchor, 478 client_view_->GetNativeView()); 479 } 480 481 void TouchSelectionControllerImpl::StartContextMenuTimer() { 482 if (context_menu_timer_.IsRunning()) 483 return; 484 context_menu_timer_.Start( 485 FROM_HERE, 486 base::TimeDelta::FromMilliseconds(kContextMenuTimoutMs), 487 this, 488 &TouchSelectionControllerImpl::ContextMenuTimerFired); 489 } 490 491 void TouchSelectionControllerImpl::UpdateContextMenu(const gfx::Point& p1, 492 const gfx::Point& p2) { 493 // Hide context menu to be shown when the timer fires. 494 HideContextMenu(); 495 StartContextMenuTimer(); 496 } 497 498 void TouchSelectionControllerImpl::HideContextMenu() { 499 if (context_menu_) 500 context_menu_->Close(); 501 context_menu_ = NULL; 502 context_menu_timer_.Stop(); 503 } 504 505 gfx::Point TouchSelectionControllerImpl::GetSelectionHandle1Position() { 506 return selection_handle_1_->GetScreenPosition(); 507 } 508 509 gfx::Point TouchSelectionControllerImpl::GetSelectionHandle2Position() { 510 return selection_handle_2_->GetScreenPosition(); 511 } 512 513 gfx::Point TouchSelectionControllerImpl::GetCursorHandlePosition() { 514 return cursor_handle_->GetScreenPosition(); 515 } 516 517 bool TouchSelectionControllerImpl::IsSelectionHandle1Visible() { 518 return selection_handle_1_->visible(); 519 } 520 521 bool TouchSelectionControllerImpl::IsSelectionHandle2Visible() { 522 return selection_handle_2_->visible(); 523 } 524 525 bool TouchSelectionControllerImpl::IsCursorHandleVisible() { 526 return cursor_handle_->visible(); 527 } 528 529 ViewsTouchSelectionControllerFactory::ViewsTouchSelectionControllerFactory() { 530 } 531 532 ui::TouchSelectionController* ViewsTouchSelectionControllerFactory::create( 533 ui::TouchEditable* client_view) { 534 if (switches::IsTouchEditingEnabled()) 535 return new views::TouchSelectionControllerImpl(client_view); 536 return NULL; 537 } 538 539 } // namespace views 540