Home | History | Annotate | Download | only in touchui
      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