// 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/aura/client/cursor_client.h" #include "ui/aura/env.h" #include "ui/aura/window.h" #include "ui/base/resource/resource_bundle.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/widget/widget.h" #include "ui/wm/core/masked_window_targeter.h" #include "ui/wm/core/window_animations.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; const int kSelectionHandleQuickFadeDurationMs = 50; // Minimum height for selection handle bar. If the bar height is going to be // less than this value, handle will not be shown. const int kSelectionHandleBarMinHeight = 5; // Maximum amount that selection handle bar can stick out of client view's // boundaries. const int kSelectionHandleBarBottomAllowance = 3; // 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_POPUP); params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; params.shadow_type = views::Widget::InitParams::SHADOW_TYPE_NONE; params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; params.parent = context; params.delegate = widget_delegate; widget->Init(params); 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 methods to convert a |rect| from screen to the |client|'s // coordinate system and vice versa. // 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()); } gfx::Rect ConvertToScreen(ui::TouchEditable* client, const gfx::Rect& rect) { gfx::Point origin = rect.origin(); client->ConvertPointToScreen(&origin); return gfx::Rect(origin, rect.size()); } } // namespace namespace views { typedef TouchSelectionControllerImpl::EditingHandleView EditingHandleView; class TouchHandleWindowTargeter : public wm::MaskedWindowTargeter { public: TouchHandleWindowTargeter(aura::Window* window, EditingHandleView* handle_view); virtual ~TouchHandleWindowTargeter() {} private: // wm::MaskedWindowTargeter: virtual bool GetHitTestMask(aura::Window* window, gfx::Path* mask) const OVERRIDE; EditingHandleView* handle_view_; DISALLOW_COPY_AND_ASSIGN(TouchHandleWindowTargeter); }; // A View that displays the text selection handle. class TouchSelectionControllerImpl::EditingHandleView : public views::WidgetDelegateView { public: EditingHandleView(TouchSelectionControllerImpl* controller, gfx::NativeView context) : controller_(controller), drag_offset_(0), draw_invisible_(false) { widget_.reset(CreateTouchSelectionPopupWidget(context, this)); widget_->SetContentsView(this); aura::Window* window = widget_->GetNativeWindow(); window->SetEventTargeter(scoped_ptr( new TouchHandleWindowTargeter(window, this))); // We are owned by the TouchSelectionController. set_owned_by_client(); } virtual ~EditingHandleView() { SetWidgetVisible(false, false); } // 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() const 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, bool quick) { if (widget_->IsVisible() == visible) return; wm::SetWindowVisibilityAnimationDuration( widget_->GetNativeView(), base::TimeDelta::FromMilliseconds( quick ? kSelectionHandleQuickFadeDurationMs : 0)); 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(); } const gfx::Rect& selection_rect() const { return selection_rect_; } 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); }; TouchHandleWindowTargeter::TouchHandleWindowTargeter( aura::Window* window, EditingHandleView* handle_view) : wm::MaskedWindowTargeter(window), handle_view_(handle_view) { } bool TouchHandleWindowTargeter::GetHitTestMask(aura::Window* window, gfx::Path* mask) const { const gfx::Rect& selection_rect = handle_view_->selection_rect(); 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)); return true; } 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); aura::Env::GetInstance()->AddPreTargetHandler(this); } TouchSelectionControllerImpl::~TouchSelectionControllerImpl() { HideContextMenu(); aura::Env::GetInstance()->RemovePreTargetHandler(this); if (client_widget_) client_widget_->RemoveObserver(this); } void TouchSelectionControllerImpl::SelectionChanged() { gfx::Rect r1, r2; client_view_->GetSelectionEndPoints(&r1, &r2); gfx::Rect screen_rect_1 = ConvertToScreen(client_view_, r1); gfx::Rect screen_rect_2 = ConvertToScreen(client_view_, r2); gfx::Rect client_bounds = client_view_->GetBounds(); if (r1.y() < client_bounds.y()) r1.Inset(0, client_bounds.y() - r1.y(), 0, 0); if (r2.y() < client_bounds.y()) r2.Inset(0, client_bounds.y() - r2.y(), 0, 0); gfx::Rect screen_rect_1_clipped = ConvertToScreen(client_view_, r1); gfx::Rect screen_rect_2_clipped = ConvertToScreen(client_view_, r2); if (screen_rect_1_clipped == selection_end_point_1_clipped_ && screen_rect_2_clipped == selection_end_point_2_clipped_) return; selection_end_point_1_ = screen_rect_1; selection_end_point_2_ = screen_rect_2; selection_end_point_1_clipped_ = screen_rect_1_clipped; selection_end_point_2_clipped_ = screen_rect_2_clipped; if (client_view_->DrawsHandles()) { UpdateContextMenu(); 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_clipped); // 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(!ShouldShowHandleFor(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; selection_end_point_1_clipped_ = screen_rect_2_clipped; selection_end_point_2_clipped_ = screen_rect_1_clipped; } SetHandleSelectionRect(non_dragging_handle, r1, screen_rect_1_clipped); } } else { UpdateContextMenu(); // Check if there is any selection at all. if (screen_rect_1.origin() == screen_rect_2.origin()) { selection_handle_1_->SetWidgetVisible(false, false); selection_handle_2_->SetWidgetVisible(false, false); SetHandleSelectionRect(cursor_handle_.get(), r1, screen_rect_1_clipped); return; } cursor_handle_->SetWidgetVisible(false, false); SetHandleSelectionRect(selection_handle_1_.get(), r1, screen_rect_1_clipped); SetHandleSelectionRect(selection_handle_2_.get(), r2, screen_rect_2_clipped); } } bool TouchSelectionControllerImpl::IsHandleDragInProgress() { return !!dragging_handle_; } void TouchSelectionControllerImpl::HideHandles(bool quick) { selection_handle_1_->SetWidgetVisible(false, quick); selection_handle_2_->SetWidgetVisible(false, quick); cursor_handle_->SetWidgetVisible(false, quick); } 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(ShouldShowHandleFor(rect), false); if (handle->IsWidgetVisible()) handle->SetSelectionRectInScreen(rect_in_screen); } bool TouchSelectionControllerImpl::ShouldShowHandleFor( const gfx::Rect& rect) const { if (rect.height() < kSelectionHandleBarMinHeight) return false; gfx::Rect bounds = client_view_->GetBounds(); bounds.Inset(0, 0, 0, -kSelectionHandleBarBottomAllowance); return bounds.Contains(rect); } 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::OnKeyEvent(ui::KeyEvent* event) { client_view_->DestroyTouchSelection(); } void TouchSelectionControllerImpl::OnMouseEvent(ui::MouseEvent* event) { aura::client::CursorClient* cursor_client = aura::client::GetCursorClient( client_view_->GetNativeView()->GetRootWindow()); if (!cursor_client || cursor_client->IsMouseEventsEnabled()) client_view_->DestroyTouchSelection(); } void TouchSelectionControllerImpl::OnScrollEvent(ui::ScrollEvent* event) { client_view_->DestroyTouchSelection(); } 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_clipped_; end_rect_2_in_screen = end_rect_1_in_screen; } else { end_rect_1_in_screen = selection_end_point_1_clipped_; end_rect_2_in_screen = selection_end_point_2_clipped_; } // 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; if (ShouldShowHandleFor(end_rect_1) && ShouldShowHandleFor(end_rect_2)) menu_anchor = Union(end_rect_1_in_screen,end_rect_2_in_screen); else if (ShouldShowHandleFor(end_rect_1)) menu_anchor = end_rect_1_in_screen; else if (ShouldShowHandleFor(end_rect_2)) menu_anchor = end_rect_2_in_screen; else return; DCHECK(!context_menu_); context_menu_ = TouchEditingMenuView::Create(this, menu_anchor, GetHandleImageSize(), 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() { // 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::NativeView TouchSelectionControllerImpl::GetCursorHandleNativeView() { return cursor_handle_->GetWidget()->GetNativeView(); } 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(); } } // namespace views