touch_selection_controller_impl.cc revision eb525c5499e34cc9c4b825d6d9e75bb07cc06ace
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 21namespace { 22 23// Constants defining the visual attributes of selection handles 24const int kSelectionHandleLineWidth = 1; 25const SkColor kSelectionHandleLineColor = 26 SkColorSetRGB(0x42, 0x81, 0xf4); 27 28// Padding around the selection handle defining the area that will be included 29// in the touch target to make dragging the handle easier. 30const int kSelectionHandlePadding = 10; 31 32// The minimum selection size to trigger selection controller. 33const int kMinSelectionSize = 4; 34 35const int kContextMenuTimoutMs = 200; 36 37// Creates a widget to host SelectionHandleView. 38views::Widget* CreateTouchSelectionPopupWidget( 39 gfx::NativeView context, 40 views::WidgetDelegate* widget_delegate) { 41 views::Widget* widget = new views::Widget; 42 views::Widget::InitParams params(views::Widget::InitParams::TYPE_TOOLTIP); 43 params.can_activate = false; 44 params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; 45 params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; 46 params.context = context; 47 params.delegate = widget_delegate; 48 widget->Init(params); 49#if defined(USE_AURA) 50 SetShadowType(widget->GetNativeView(), views::corewm::SHADOW_TYPE_NONE); 51#endif 52 return widget; 53} 54 55gfx::Image* GetHandleImage() { 56 static gfx::Image* handle_image = NULL; 57 if (!handle_image) { 58 handle_image = &ui::ResourceBundle::GetSharedInstance().GetImageNamed( 59 IDR_TEXT_SELECTION_HANDLE); 60 } 61 return handle_image; 62} 63 64gfx::Size GetHandleImageSize() { 65 return GetHandleImage()->Size(); 66} 67 68// The points may not match exactly, since the selection range computation may 69// introduce some floating point errors. So check for a minimum size to decide 70// whether or not there is any selection. 71bool IsEmptySelection(const gfx::Point& p1, const gfx::Point& p2) { 72 int delta_x = p2.x() - p1.x(); 73 int delta_y = p2.y() - p1.y(); 74 return (abs(delta_x) < kMinSelectionSize && abs(delta_y) < kMinSelectionSize); 75} 76 77} // namespace 78 79namespace views { 80 81// A View that displays the text selection handle. 82class TouchSelectionControllerImpl::EditingHandleView 83 : public views::WidgetDelegateView { 84 public: 85 explicit EditingHandleView(TouchSelectionControllerImpl* controller, 86 gfx::NativeView context) 87 : controller_(controller), 88 cursor_height_(0) { 89 widget_.reset(CreateTouchSelectionPopupWidget(context, this)); 90 widget_->SetContentsView(this); 91 widget_->SetAlwaysOnTop(true); 92 93 // We are owned by the TouchSelectionController. 94 set_owned_by_client(); 95 } 96 97 virtual ~EditingHandleView() { 98 } 99 100 int cursor_height() const { return cursor_height_; } 101 102 // Overridden from views::WidgetDelegateView: 103 virtual bool WidgetHasHitTestMask() const OVERRIDE { 104 return true; 105 } 106 107 virtual void GetWidgetHitTestMask(gfx::Path* mask) const OVERRIDE { 108 gfx::Size image_size = GetHandleImageSize(); 109 mask->addRect(SkIntToScalar(0), SkIntToScalar(cursor_height_), 110 SkIntToScalar(image_size.width()) + 2 * kSelectionHandlePadding, 111 SkIntToScalar(cursor_height_ + image_size.height() + 112 kSelectionHandlePadding)); 113 } 114 115 virtual void DeleteDelegate() OVERRIDE { 116 // We are owned and deleted by TouchSelectionController. 117 } 118 119 // Overridden from views::View: 120 virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE { 121 gfx::Size image_size = GetHandleImageSize(); 122 int cursor_pos_x = image_size.width() / 2 - kSelectionHandleLineWidth + 123 kSelectionHandlePadding; 124 125 // Draw the cursor line. 126 canvas->FillRect( 127 gfx::Rect(cursor_pos_x, 0, 128 2 * kSelectionHandleLineWidth + 1, cursor_height_), 129 kSelectionHandleLineColor); 130 131 // Draw the handle image. 132 canvas->DrawImageInt(*GetHandleImage()->ToImageSkia(), 133 kSelectionHandlePadding, cursor_height_); 134 } 135 136 virtual void OnGestureEvent(ui::GestureEvent* event) OVERRIDE { 137 event->SetHandled(); 138 switch (event->type()) { 139 case ui::ET_GESTURE_SCROLL_BEGIN: 140 controller_->SetDraggingHandle(this); 141 break; 142 case ui::ET_GESTURE_SCROLL_UPDATE: 143 controller_->SelectionHandleDragged(event->location()); 144 break; 145 case ui::ET_GESTURE_SCROLL_END: 146 controller_->SetDraggingHandle(NULL); 147 break; 148 default: 149 break; 150 } 151 } 152 153 virtual void SetVisible(bool visible) OVERRIDE { 154 // We simply show/hide the container widget. 155 if (visible != widget_->IsVisible()) { 156 if (visible) 157 widget_->Show(); 158 else 159 widget_->Hide(); 160 } 161 View::SetVisible(visible); 162 } 163 164 virtual gfx::Size GetPreferredSize() OVERRIDE { 165 gfx::Size image_size = GetHandleImageSize(); 166 return gfx::Size(image_size.width() + 2 * kSelectionHandlePadding, 167 image_size.height() + cursor_height_ + kSelectionHandlePadding); 168 } 169 170 bool IsWidgetVisible() const { 171 return widget_->IsVisible(); 172 } 173 174 void SetSelectionRectInScreen(const gfx::Rect& rect) { 175 gfx::Size image_size = GetHandleImageSize(); 176 cursor_height_ = rect.height(); 177 gfx::Rect widget_bounds( 178 rect.x() - image_size.width() / 2 - kSelectionHandlePadding, 179 rect.y(), 180 image_size.width() + 2 * kSelectionHandlePadding, 181 rect.height() + image_size.height() + kSelectionHandlePadding); 182 widget_->SetBounds(widget_bounds); 183 } 184 185 gfx::Point GetScreenPosition() { 186 return widget_->GetClientAreaBoundsInScreen().origin(); 187 } 188 189 private: 190 scoped_ptr<Widget> widget_; 191 TouchSelectionControllerImpl* controller_; 192 int cursor_height_; 193 194 DISALLOW_COPY_AND_ASSIGN(EditingHandleView); 195}; 196 197TouchSelectionControllerImpl::TouchSelectionControllerImpl( 198 ui::TouchEditable* client_view) 199 : client_view_(client_view), 200 client_widget_(NULL), 201 selection_handle_1_(new EditingHandleView(this, 202 client_view->GetNativeView())), 203 selection_handle_2_(new EditingHandleView(this, 204 client_view->GetNativeView())), 205 cursor_handle_(new EditingHandleView(this, 206 client_view->GetNativeView())), 207 context_menu_(NULL), 208 dragging_handle_(NULL) { 209 client_widget_ = Widget::GetTopLevelWidgetForNativeView( 210 client_view_->GetNativeView()); 211 if (client_widget_) 212 client_widget_->AddObserver(this); 213} 214 215TouchSelectionControllerImpl::~TouchSelectionControllerImpl() { 216 HideContextMenu(); 217 if (client_widget_) 218 client_widget_->RemoveObserver(this); 219} 220 221void TouchSelectionControllerImpl::SelectionChanged() { 222 gfx::Rect r1, r2; 223 client_view_->GetSelectionEndPoints(&r1, &r2); 224 gfx::Point screen_pos_1(r1.origin()); 225 client_view_->ConvertPointToScreen(&screen_pos_1); 226 gfx::Point screen_pos_2(r2.origin()); 227 client_view_->ConvertPointToScreen(&screen_pos_2); 228 gfx::Rect screen_rect_1(screen_pos_1, r1.size()); 229 gfx::Rect screen_rect_2(screen_pos_2, r2.size()); 230 231 if (client_view_->DrawsHandles()) { 232 UpdateContextMenu(r1.origin(), r2.origin()); 233 return; 234 } 235 if (dragging_handle_) { 236 // We need to reposition only the selection handle that is being dragged. 237 // The other handle stays the same. Also, the selection handle being dragged 238 // will always be at the end of selection, while the other handle will be at 239 // the start. 240 dragging_handle_->SetSelectionRectInScreen(screen_rect_2); 241 242 if (dragging_handle_ != cursor_handle_.get()) { 243 // The non-dragging-handle might have recently become visible. 244 EditingHandleView* non_dragging_handle = 245 dragging_handle_ == selection_handle_1_.get()? 246 selection_handle_2_.get() : selection_handle_1_.get(); 247 if (client_view_->GetBounds().Contains(r1.origin())) { 248 non_dragging_handle->SetSelectionRectInScreen(screen_rect_1); 249 non_dragging_handle->SetVisible(true); 250 } else { 251 non_dragging_handle->SetVisible(false); 252 } 253 } 254 } else { 255 UpdateContextMenu(r1.origin(), r2.origin()); 256 257 // Check if there is any selection at all. 258 if (IsEmptySelection(screen_pos_2, screen_pos_1)) { 259 selection_handle_1_->SetVisible(false); 260 selection_handle_2_->SetVisible(false); 261 cursor_handle_->SetSelectionRectInScreen(screen_rect_1); 262 cursor_handle_->SetVisible(true); 263 return; 264 } 265 266 cursor_handle_->SetVisible(false); 267 if (client_view_->GetBounds().Contains(r1.origin())) { 268 selection_handle_1_->SetSelectionRectInScreen(screen_rect_1); 269 selection_handle_1_->SetVisible(true); 270 } else { 271 selection_handle_1_->SetVisible(false); 272 } 273 274 if (client_view_->GetBounds().Contains(r2.origin())) { 275 selection_handle_2_->SetSelectionRectInScreen(screen_rect_2); 276 selection_handle_2_->SetVisible(true); 277 } else { 278 selection_handle_2_->SetVisible(false); 279 } 280 } 281} 282 283bool TouchSelectionControllerImpl::IsHandleDragInProgress() { 284 return !!dragging_handle_; 285} 286 287void TouchSelectionControllerImpl::SetDraggingHandle( 288 EditingHandleView* handle) { 289 dragging_handle_ = handle; 290 if (dragging_handle_) 291 HideContextMenu(); 292 else 293 StartContextMenuTimer(); 294} 295 296void TouchSelectionControllerImpl::SelectionHandleDragged( 297 const gfx::Point& drag_pos) { 298 // We do not want to show the context menu while dragging. 299 HideContextMenu(); 300 301 DCHECK(dragging_handle_); 302 303 gfx::Size image_size = GetHandleImageSize(); 304 gfx::Point offset_drag_pos(drag_pos.x(), 305 drag_pos.y() - dragging_handle_->cursor_height() / 2 - 306 image_size.height() / 2); 307 ConvertPointToClientView(dragging_handle_, &offset_drag_pos); 308 if (dragging_handle_ == cursor_handle_.get()) { 309 client_view_->MoveCaretTo(offset_drag_pos); 310 return; 311 } 312 313 // Find the stationary selection handle. 314 EditingHandleView* fixed_handle = selection_handle_1_.get(); 315 if (fixed_handle == dragging_handle_) 316 fixed_handle = selection_handle_2_.get(); 317 318 // Find selection end points in client_view's coordinate system. 319 gfx::Point p2(image_size.width() / 2 + kSelectionHandlePadding, 320 fixed_handle->cursor_height() / 2); 321 ConvertPointToClientView(fixed_handle, &p2); 322 323 // Instruct client_view to select the region between p1 and p2. The position 324 // of |fixed_handle| is the start and that of |dragging_handle| is the end 325 // of selection. 326 client_view_->SelectRect(p2, offset_drag_pos); 327} 328 329void TouchSelectionControllerImpl::ConvertPointToClientView( 330 EditingHandleView* source, gfx::Point* point) { 331 View::ConvertPointToScreen(source, point); 332 client_view_->ConvertPointFromScreen(point); 333} 334 335bool TouchSelectionControllerImpl::IsCommandIdEnabled(int command_id) const { 336 return client_view_->IsCommandIdEnabled(command_id); 337} 338 339void TouchSelectionControllerImpl::ExecuteCommand(int command_id, 340 int event_flags) { 341 HideContextMenu(); 342 client_view_->ExecuteCommand(command_id, event_flags); 343} 344 345void TouchSelectionControllerImpl::OpenContextMenu() { 346 gfx::Size image_size = GetHandleImageSize(); 347 gfx::Point anchor = context_menu_->anchor_rect().CenterPoint(); 348 anchor.Offset(0, -image_size.height() / 2); 349 HideContextMenu(); 350 client_view_->OpenContextMenu(anchor); 351} 352 353void TouchSelectionControllerImpl::OnMenuClosed(TouchEditingMenuView* menu) { 354 if (menu == context_menu_) 355 context_menu_ = NULL; 356} 357 358void TouchSelectionControllerImpl::OnWidgetClosing(Widget* widget) { 359 DCHECK_EQ(client_widget_, widget); 360 client_widget_ = NULL; 361} 362 363void TouchSelectionControllerImpl::OnWidgetBoundsChanged( 364 Widget* widget, 365 const gfx::Rect& new_bounds) { 366 DCHECK_EQ(client_widget_, widget); 367 HideContextMenu(); 368 SelectionChanged(); 369} 370 371void TouchSelectionControllerImpl::ContextMenuTimerFired() { 372 // Get selection end points in client_view's space. 373 gfx::Rect r1, r2; 374 client_view_->GetSelectionEndPoints(&r1, &r2); 375 376 gfx::Rect handle_1_bounds; 377 gfx::Rect handle_2_bounds; 378 if (cursor_handle_->IsWidgetVisible()) { 379 handle_1_bounds = cursor_handle_->GetBoundsInScreen(); 380 handle_2_bounds = handle_1_bounds; 381 } else { 382 handle_1_bounds = selection_handle_1_->GetBoundsInScreen(); 383 handle_2_bounds = selection_handle_2_->GetBoundsInScreen(); 384 } 385 386 // if selection is completely inside the view, we display the context menu 387 // in the middle of the end points on the top. Else, we show it above the 388 // visible handle. If no handle is visible, we do not show the menu. 389 gfx::Rect menu_anchor; 390 gfx::Rect client_bounds = client_view_->GetBounds(); 391 if (client_bounds.Contains(r1.origin()) && 392 client_bounds.Contains(r2.origin())) { 393 menu_anchor = gfx::UnionRects(handle_1_bounds, handle_2_bounds); 394 } else if (client_bounds.Contains(r1.origin())) { 395 menu_anchor = handle_1_bounds; 396 } else if (client_bounds.Contains(r2.origin())) { 397 menu_anchor = handle_2_bounds; 398 } else { 399 return; 400 } 401 402 DCHECK(!context_menu_); 403 context_menu_ = new TouchEditingMenuView(this, menu_anchor, 404 client_view_->GetNativeView()); 405} 406 407void TouchSelectionControllerImpl::StartContextMenuTimer() { 408 if (context_menu_timer_.IsRunning()) 409 return; 410 context_menu_timer_.Start( 411 FROM_HERE, 412 base::TimeDelta::FromMilliseconds(kContextMenuTimoutMs), 413 this, 414 &TouchSelectionControllerImpl::ContextMenuTimerFired); 415} 416 417void TouchSelectionControllerImpl::UpdateContextMenu(const gfx::Point& p1, 418 const gfx::Point& p2) { 419 // Hide context menu to be shown when the timer fires. 420 HideContextMenu(); 421 StartContextMenuTimer(); 422} 423 424void TouchSelectionControllerImpl::HideContextMenu() { 425 if (context_menu_) 426 context_menu_->Close(); 427 context_menu_ = NULL; 428 context_menu_timer_.Stop(); 429} 430 431gfx::Point TouchSelectionControllerImpl::GetSelectionHandle1Position() { 432 return selection_handle_1_->GetScreenPosition(); 433} 434 435gfx::Point TouchSelectionControllerImpl::GetSelectionHandle2Position() { 436 return selection_handle_2_->GetScreenPosition(); 437} 438 439gfx::Point TouchSelectionControllerImpl::GetCursorHandlePosition() { 440 return cursor_handle_->GetScreenPosition(); 441} 442 443bool TouchSelectionControllerImpl::IsSelectionHandle1Visible() { 444 return selection_handle_1_->visible(); 445} 446 447bool TouchSelectionControllerImpl::IsSelectionHandle2Visible() { 448 return selection_handle_2_->visible(); 449} 450 451bool TouchSelectionControllerImpl::IsCursorHandleVisible() { 452 return cursor_handle_->visible(); 453} 454 455ViewsTouchSelectionControllerFactory::ViewsTouchSelectionControllerFactory() { 456} 457 458ui::TouchSelectionController* ViewsTouchSelectionControllerFactory::create( 459 ui::TouchEditable* client_view) { 460 if (switches::IsTouchEditingEnabled()) 461 return new views::TouchSelectionControllerImpl(client_view); 462 return NULL; 463} 464 465} // namespace views 466