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