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