// Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ui/views/touchui/touch_selection_controller_impl.h" #include "base/time/time.h" #include "grit/ui_resources.h" #include "grit/ui_strings.h" #include "ui/base/resource/resource_bundle.h" #include "ui/base/ui_base_switches_util.h" #include "ui/gfx/canvas.h" #include "ui/gfx/image/image.h" #include "ui/gfx/path.h" #include "ui/gfx/rect.h" #include "ui/gfx/screen.h" #include "ui/gfx/size.h" #include "ui/views/corewm/shadow_types.h" #include "ui/views/widget/widget.h" namespace { // Constants defining the visual attributes of selection handles const int kSelectionHandleLineWidth = 1; const SkColor kSelectionHandleLineColor = SkColorSetRGB(0x42, 0x81, 0xf4); // When a handle is dragged, the drag position reported to the client view is // offset vertically to represent the cursor position. This constant specifies // the offset in pixels above the "O" (see pic below). This is required because // say if this is zero, that means the drag position we report is the point // right above the "O" or the bottom most point of the cursor "|". In that case, // a vertical movement of even one pixel will make the handle jump to the line // below it. So when the user just starts dragging, the handle will jump to the // next line if the user makes any vertical movement. It is correct but // looks/feels weird. So we have this non-zero offset to prevent this jumping. // // Editing handle widget showing the difference between the position of the // ET_GESTURE_SCROLL_UPDATE event and the drag position reported to the client: // _____ // | |<-|---- Drag position reported to client // _ | O | // Vertical Padding __| | <-|---- ET_GESTURE_SCROLL_UPDATE position // |_ |_____|<--- Editing handle widget // // | | // T // Horizontal Padding // const int kSelectionHandleVerticalDragOffset = 5; // Padding around the selection handle defining the area that will be included // in the touch target to make dragging the handle easier (see pic above). const int kSelectionHandleHorizPadding = 10; const int kSelectionHandleVertPadding = 20; const int kContextMenuTimoutMs = 200; // Creates a widget to host SelectionHandleView. views::Widget* CreateTouchSelectionPopupWidget( gfx::NativeView context, views::WidgetDelegate* widget_delegate) { views::Widget* widget = new views::Widget; views::Widget::InitParams params(views::Widget::InitParams::TYPE_TOOLTIP); params.can_activate = false; params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; params.context = context; params.delegate = widget_delegate; widget->Init(params); #if defined(USE_AURA) SetShadowType(widget->GetNativeView(), views::corewm::SHADOW_TYPE_NONE); #endif return widget; } gfx::Image* GetHandleImage() { static gfx::Image* handle_image = NULL; if (!handle_image) { handle_image = &ui::ResourceBundle::GetSharedInstance().GetImageNamed( IDR_TEXT_SELECTION_HANDLE); } return handle_image; } gfx::Size GetHandleImageSize() { return GetHandleImage()->Size(); } // Cannot use gfx::UnionRect since it does not work for empty rects. gfx::Rect Union(const gfx::Rect& r1, const gfx::Rect& r2) { int rx = std::min(r1.x(), r2.x()); int ry = std::min(r1.y(), r2.y()); int rr = std::max(r1.right(), r2.right()); int rb = std::max(r1.bottom(), r2.bottom()); return gfx::Rect(rx, ry, rr - rx, rb - ry); } // Convenience method to convert a |rect| from screen to the |client|'s // coordinate system. // Note that this is not quite correct because it does not take into account // transforms such as rotation and scaling. This should be in TouchEditable. // TODO(varunjain): Fix this. gfx::Rect ConvertFromScreen(ui::TouchEditable* client, const gfx::Rect& rect) { gfx::Point origin = rect.origin(); client->ConvertPointFromScreen(&origin); return gfx::Rect(origin, rect.size()); } } // namespace namespace views { // A View that displays the text selection handle. class TouchSelectionControllerImpl::EditingHandleView : public views::WidgetDelegateView { public: explicit EditingHandleView(TouchSelectionControllerImpl* controller, gfx::NativeView context) : controller_(controller), drag_offset_(0), draw_invisible_(false) { widget_.reset(CreateTouchSelectionPopupWidget(context, this)); widget_->SetContentsView(this); widget_->SetAlwaysOnTop(true); // We are owned by the TouchSelectionController. set_owned_by_client(); } virtual ~EditingHandleView() { } // Overridden from views::WidgetDelegateView: virtual bool WidgetHasHitTestMask() const OVERRIDE { return true; } virtual void GetWidgetHitTestMask(gfx::Path* mask) const OVERRIDE { gfx::Size image_size = GetHandleImageSize(); mask->addRect(SkIntToScalar(0), SkIntToScalar(selection_rect_.height()), SkIntToScalar(image_size.width()) + 2 * kSelectionHandleHorizPadding, SkIntToScalar(selection_rect_.height() + image_size.height() + kSelectionHandleVertPadding)); } virtual void DeleteDelegate() OVERRIDE { // We are owned and deleted by TouchSelectionController. } // Overridden from views::View: virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE { if (draw_invisible_) return; gfx::Size image_size = GetHandleImageSize(); int cursor_pos_x = image_size.width() / 2 - kSelectionHandleLineWidth + kSelectionHandleHorizPadding; // Draw the cursor line. canvas->FillRect( gfx::Rect(cursor_pos_x, 0, 2 * kSelectionHandleLineWidth + 1, selection_rect_.height()), kSelectionHandleLineColor); // Draw the handle image. canvas->DrawImageInt(*GetHandleImage()->ToImageSkia(), kSelectionHandleHorizPadding, selection_rect_.height()); } virtual void OnGestureEvent(ui::GestureEvent* event) OVERRIDE { event->SetHandled(); switch (event->type()) { case ui::ET_GESTURE_SCROLL_BEGIN: widget_->SetCapture(this); controller_->SetDraggingHandle(this); drag_offset_ = event->y() - selection_rect_.height() + kSelectionHandleVerticalDragOffset; break; case ui::ET_GESTURE_SCROLL_UPDATE: { gfx::Point drag_pos(event->location().x(), event->location().y() - drag_offset_); controller_->SelectionHandleDragged(drag_pos); break; } case ui::ET_GESTURE_SCROLL_END: case ui::ET_SCROLL_FLING_START: widget_->ReleaseCapture(); controller_->SetDraggingHandle(NULL); break; default: break; } } virtual gfx::Size GetPreferredSize() OVERRIDE { gfx::Size image_size = GetHandleImageSize(); return gfx::Size(image_size.width() + 2 * kSelectionHandleHorizPadding, image_size.height() + selection_rect_.height() + kSelectionHandleVertPadding); } bool IsWidgetVisible() const { return widget_->IsVisible(); } void SetWidgetVisible(bool visible) { if (widget_->IsVisible() == visible) return; if (visible) widget_->Show(); else widget_->Hide(); } void SetSelectionRectInScreen(const gfx::Rect& rect) { gfx::Size image_size = GetHandleImageSize(); selection_rect_ = rect; gfx::Rect widget_bounds( rect.x() - image_size.width() / 2 - kSelectionHandleHorizPadding, rect.y(), image_size.width() + 2 * kSelectionHandleHorizPadding, rect.height() + image_size.height() + kSelectionHandleVertPadding); widget_->SetBounds(widget_bounds); } gfx::Point GetScreenPosition() { return widget_->GetClientAreaBoundsInScreen().origin(); } void SetDrawInvisible(bool draw_invisible) { if (draw_invisible_ == draw_invisible) return; draw_invisible_ = draw_invisible; SchedulePaint(); } private: scoped_ptr widget_; TouchSelectionControllerImpl* controller_; gfx::Rect selection_rect_; // Vertical offset between the scroll event position and the drag position // reported to the client view (see the ASCII figure at the top of the file // and its description for more details). int drag_offset_; // If set to true, the handle will not draw anything, hence providing an empty // widget. We need this because we may want to stop showing the handle while // it is being dragged. Since it is being dragged, we cannot destroy the // handle. bool draw_invisible_; DISALLOW_COPY_AND_ASSIGN(EditingHandleView); }; TouchSelectionControllerImpl::TouchSelectionControllerImpl( ui::TouchEditable* client_view) : client_view_(client_view), client_widget_(NULL), selection_handle_1_(new EditingHandleView(this, client_view->GetNativeView())), selection_handle_2_(new EditingHandleView(this, client_view->GetNativeView())), cursor_handle_(new EditingHandleView(this, client_view->GetNativeView())), context_menu_(NULL), dragging_handle_(NULL) { client_widget_ = Widget::GetTopLevelWidgetForNativeView( client_view_->GetNativeView()); if (client_widget_) client_widget_->AddObserver(this); } TouchSelectionControllerImpl::~TouchSelectionControllerImpl() { HideContextMenu(); if (client_widget_) client_widget_->RemoveObserver(this); } void TouchSelectionControllerImpl::SelectionChanged() { gfx::Rect r1, r2; client_view_->GetSelectionEndPoints(&r1, &r2); gfx::Point screen_pos_1(r1.origin()); client_view_->ConvertPointToScreen(&screen_pos_1); gfx::Point screen_pos_2(r2.origin()); client_view_->ConvertPointToScreen(&screen_pos_2); gfx::Rect screen_rect_1(screen_pos_1, r1.size()); gfx::Rect screen_rect_2(screen_pos_2, r2.size()); if (screen_rect_1 == selection_end_point_1_ && screen_rect_2 == selection_end_point_2_) return; selection_end_point_1_ = screen_rect_1; selection_end_point_2_ = screen_rect_2; if (client_view_->DrawsHandles()) { UpdateContextMenu(r1.origin(), r2.origin()); return; } if (dragging_handle_) { // We need to reposition only the selection handle that is being dragged. // The other handle stays the same. Also, the selection handle being dragged // will always be at the end of selection, while the other handle will be at // the start. // If the new location of this handle is out of client view, its widget // should not get hidden, since it should still receive touch events. // Hence, we are not using |SetHandleSelectionRect()| method here. dragging_handle_->SetSelectionRectInScreen(screen_rect_2); // Temporary fix for selection handle going outside a window. On a webpage, // the page should scroll if the selection handle is dragged outside the // window. That does not happen currently. So we just hide the handle for // now. // TODO(varunjain): Fix this: crbug.com/269003 dragging_handle_->SetDrawInvisible(!client_view_->GetBounds().Contains(r2)); if (dragging_handle_ != cursor_handle_.get()) { // The non-dragging-handle might have recently become visible. EditingHandleView* non_dragging_handle = selection_handle_1_.get(); if (dragging_handle_ == selection_handle_1_) { non_dragging_handle = selection_handle_2_.get(); // if handle 1 is being dragged, it is corresponding to the end of // selection and the other handle to the start of selection. selection_end_point_1_ = screen_rect_2; selection_end_point_2_ = screen_rect_1; } SetHandleSelectionRect(non_dragging_handle, r1, screen_rect_1); } } else { UpdateContextMenu(r1.origin(), r2.origin()); // Check if there is any selection at all. if (screen_pos_1 == screen_pos_2) { selection_handle_1_->SetWidgetVisible(false); selection_handle_2_->SetWidgetVisible(false); SetHandleSelectionRect(cursor_handle_.get(), r1, screen_rect_1); return; } cursor_handle_->SetWidgetVisible(false); SetHandleSelectionRect(selection_handle_1_.get(), r1, screen_rect_1); SetHandleSelectionRect(selection_handle_2_.get(), r2, screen_rect_2); } } bool TouchSelectionControllerImpl::IsHandleDragInProgress() { return !!dragging_handle_; } void TouchSelectionControllerImpl::SetDraggingHandle( EditingHandleView* handle) { dragging_handle_ = handle; if (dragging_handle_) HideContextMenu(); else StartContextMenuTimer(); } void TouchSelectionControllerImpl::SelectionHandleDragged( const gfx::Point& drag_pos) { // We do not want to show the context menu while dragging. HideContextMenu(); DCHECK(dragging_handle_); gfx::Point drag_pos_in_client = drag_pos; ConvertPointToClientView(dragging_handle_, &drag_pos_in_client); if (dragging_handle_ == cursor_handle_.get()) { client_view_->MoveCaretTo(drag_pos_in_client); return; } // Find the stationary selection handle. gfx::Rect fixed_handle_rect = selection_end_point_1_; if (selection_handle_1_ == dragging_handle_) fixed_handle_rect = selection_end_point_2_; // Find selection end points in client_view's coordinate system. gfx::Point p2 = fixed_handle_rect.origin(); p2.Offset(0, fixed_handle_rect.height() / 2); client_view_->ConvertPointFromScreen(&p2); // Instruct client_view to select the region between p1 and p2. The position // of |fixed_handle| is the start and that of |dragging_handle| is the end // of selection. client_view_->SelectRect(p2, drag_pos_in_client); } void TouchSelectionControllerImpl::ConvertPointToClientView( EditingHandleView* source, gfx::Point* point) { View::ConvertPointToScreen(source, point); client_view_->ConvertPointFromScreen(point); } void TouchSelectionControllerImpl::SetHandleSelectionRect( EditingHandleView* handle, const gfx::Rect& rect, const gfx::Rect& rect_in_screen) { handle->SetWidgetVisible(client_view_->GetBounds().Contains(rect)); if (handle->IsWidgetVisible()) handle->SetSelectionRectInScreen(rect_in_screen); } bool TouchSelectionControllerImpl::IsCommandIdEnabled(int command_id) const { return client_view_->IsCommandIdEnabled(command_id); } void TouchSelectionControllerImpl::ExecuteCommand(int command_id, int event_flags) { HideContextMenu(); client_view_->ExecuteCommand(command_id, event_flags); } void TouchSelectionControllerImpl::OpenContextMenu() { // Context menu should appear centered on top of the selected region. const gfx::Rect rect = context_menu_->GetAnchorRect(); const gfx::Point anchor(rect.CenterPoint().x(), rect.y()); HideContextMenu(); client_view_->OpenContextMenu(anchor); } void TouchSelectionControllerImpl::OnMenuClosed(TouchEditingMenuView* menu) { if (menu == context_menu_) context_menu_ = NULL; } void TouchSelectionControllerImpl::OnWidgetClosing(Widget* widget) { DCHECK_EQ(client_widget_, widget); client_widget_ = NULL; } void TouchSelectionControllerImpl::OnWidgetBoundsChanged( Widget* widget, const gfx::Rect& new_bounds) { DCHECK_EQ(client_widget_, widget); HideContextMenu(); SelectionChanged(); } void TouchSelectionControllerImpl::ContextMenuTimerFired() { // Get selection end points in client_view's space. gfx::Rect end_rect_1_in_screen; gfx::Rect end_rect_2_in_screen; if (cursor_handle_->IsWidgetVisible()) { end_rect_1_in_screen = selection_end_point_1_; end_rect_2_in_screen = end_rect_1_in_screen; } else { end_rect_1_in_screen = selection_end_point_1_; end_rect_2_in_screen = selection_end_point_2_; } // Convert from screen to client. gfx::Rect end_rect_1(ConvertFromScreen(client_view_, end_rect_1_in_screen)); gfx::Rect end_rect_2(ConvertFromScreen(client_view_, end_rect_2_in_screen)); // if selection is completely inside the view, we display the context menu // in the middle of the end points on the top. Else, we show it above the // visible handle. If no handle is visible, we do not show the menu. gfx::Rect menu_anchor; gfx::Rect client_bounds = client_view_->GetBounds(); if (client_bounds.Contains(end_rect_1) && client_bounds.Contains(end_rect_2)) menu_anchor = Union(end_rect_1_in_screen,end_rect_2_in_screen); else if (client_bounds.Contains(end_rect_1)) menu_anchor = end_rect_1_in_screen; else if (client_bounds.Contains(end_rect_2)) menu_anchor = end_rect_2_in_screen; else return; DCHECK(!context_menu_); context_menu_ = TouchEditingMenuView::Create(this, menu_anchor, client_view_->GetNativeView()); } void TouchSelectionControllerImpl::StartContextMenuTimer() { if (context_menu_timer_.IsRunning()) return; context_menu_timer_.Start( FROM_HERE, base::TimeDelta::FromMilliseconds(kContextMenuTimoutMs), this, &TouchSelectionControllerImpl::ContextMenuTimerFired); } void TouchSelectionControllerImpl::UpdateContextMenu(const gfx::Point& p1, const gfx::Point& p2) { // Hide context menu to be shown when the timer fires. HideContextMenu(); StartContextMenuTimer(); } void TouchSelectionControllerImpl::HideContextMenu() { if (context_menu_) context_menu_->Close(); context_menu_ = NULL; context_menu_timer_.Stop(); } gfx::Point TouchSelectionControllerImpl::GetSelectionHandle1Position() { return selection_handle_1_->GetScreenPosition(); } gfx::Point TouchSelectionControllerImpl::GetSelectionHandle2Position() { return selection_handle_2_->GetScreenPosition(); } gfx::Point TouchSelectionControllerImpl::GetCursorHandlePosition() { return cursor_handle_->GetScreenPosition(); } bool TouchSelectionControllerImpl::IsSelectionHandle1Visible() { return selection_handle_1_->IsWidgetVisible(); } bool TouchSelectionControllerImpl::IsSelectionHandle2Visible() { return selection_handle_2_->IsWidgetVisible(); } bool TouchSelectionControllerImpl::IsCursorHandleVisible() { return cursor_handle_->IsWidgetVisible(); } ViewsTouchSelectionControllerFactory::ViewsTouchSelectionControllerFactory() { } ui::TouchSelectionController* ViewsTouchSelectionControllerFactory::create( ui::TouchEditable* client_view) { if (switches::IsTouchEditingEnabled()) return new views::TouchSelectionControllerImpl(client_view); return NULL; } } // namespace views