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.
5 #include "ui/views/touchui/touch_selection_controller_impl.h"
7 #include "base/time/time.h"
8 #include "grit/ui_resources.h"
9 #include "grit/ui_strings.h"
10 #include "ui/aura/client/cursor_client.h"
11 #include "ui/aura/env.h"
12 #include "ui/base/resource/resource_bundle.h"
13 #include "ui/gfx/canvas.h"
14 #include "ui/gfx/image/image.h"
15 #include "ui/gfx/path.h"
16 #include "ui/gfx/rect.h"
17 #include "ui/gfx/screen.h"
18 #include "ui/gfx/size.h"
19 #include "ui/views/widget/widget.h"
20 #include "ui/wm/core/masked_window_targeter.h"
21 #include "ui/wm/core/shadow_types.h"
22 #include "ui/wm/core/window_animations.h"
26 // Constants defining the visual attributes of selection handles
27 const int kSelectionHandleLineWidth
= 1;
28 const SkColor kSelectionHandleLineColor
=
29 SkColorSetRGB(0x42, 0x81, 0xf4);
31 // When a handle is dragged, the drag position reported to the client view is
32 // offset vertically to represent the cursor position. This constant specifies
33 // the offset in pixels above the "O" (see pic below). This is required because
34 // say if this is zero, that means the drag position we report is the point
35 // right above the "O" or the bottom most point of the cursor "|". In that case,
36 // a vertical movement of even one pixel will make the handle jump to the line
37 // below it. So when the user just starts dragging, the handle will jump to the
38 // next line if the user makes any vertical movement. It is correct but
39 // looks/feels weird. So we have this non-zero offset to prevent this jumping.
41 // Editing handle widget showing the difference between the position of the
42 // ET_GESTURE_SCROLL_UPDATE event and the drag position reported to the client:
44 // | |<-|---- Drag position reported to client
46 // Vertical Padding __| | <-|---- ET_GESTURE_SCROLL_UPDATE position
47 // |_ |_____|<--- Editing handle widget
53 const int kSelectionHandleVerticalDragOffset
= 5;
55 // Padding around the selection handle defining the area that will be included
56 // in the touch target to make dragging the handle easier (see pic above).
57 const int kSelectionHandleHorizPadding
= 10;
58 const int kSelectionHandleVertPadding
= 20;
60 const int kContextMenuTimoutMs
= 200;
62 const int kSelectionHandleQuickFadeDurationMs
= 50;
64 // Minimum height for selection handle bar. If the bar height is going to be
65 // less than this value, handle will not be shown.
66 const int kSelectionHandleBarMinHeight
= 5;
67 // Maximum amount that selection handle bar can stick out of client view's
69 const int kSelectionHandleBarBottomAllowance
= 3;
71 // Creates a widget to host SelectionHandleView.
72 views::Widget
* CreateTouchSelectionPopupWidget(
73 gfx::NativeView context
,
74 views::WidgetDelegate
* widget_delegate
) {
75 views::Widget
* widget
= new views::Widget
;
76 views::Widget::InitParams
params(views::Widget::InitParams::TYPE_POPUP
);
77 params
.opacity
= views::Widget::InitParams::TRANSLUCENT_WINDOW
;
78 params
.ownership
= views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET
;
79 params
.parent
= context
;
80 params
.delegate
= widget_delegate
;
82 SetShadowType(widget
->GetNativeView(), wm::SHADOW_TYPE_NONE
);
86 gfx::Image
* GetHandleImage() {
87 static gfx::Image
* handle_image
= NULL
;
89 handle_image
= &ui::ResourceBundle::GetSharedInstance().GetImageNamed(
90 IDR_TEXT_SELECTION_HANDLE
);
95 gfx::Size
GetHandleImageSize() {
96 return GetHandleImage()->Size();
99 // Cannot use gfx::UnionRect since it does not work for empty rects.
100 gfx::Rect
Union(const gfx::Rect
& r1
, const gfx::Rect
& r2
) {
101 int rx
= std::min(r1
.x(), r2
.x());
102 int ry
= std::min(r1
.y(), r2
.y());
103 int rr
= std::max(r1
.right(), r2
.right());
104 int rb
= std::max(r1
.bottom(), r2
.bottom());
106 return gfx::Rect(rx
, ry
, rr
- rx
, rb
- ry
);
109 // Convenience methods to convert a |rect| from screen to the |client|'s
110 // coordinate system and vice versa.
111 // Note that this is not quite correct because it does not take into account
112 // transforms such as rotation and scaling. This should be in TouchEditable.
113 // TODO(varunjain): Fix this.
114 gfx::Rect
ConvertFromScreen(ui::TouchEditable
* client
, const gfx::Rect
& rect
) {
115 gfx::Point origin
= rect
.origin();
116 client
->ConvertPointFromScreen(&origin
);
117 return gfx::Rect(origin
, rect
.size());
119 gfx::Rect
ConvertToScreen(ui::TouchEditable
* client
, const gfx::Rect
& rect
) {
120 gfx::Point origin
= rect
.origin();
121 client
->ConvertPointToScreen(&origin
);
122 return gfx::Rect(origin
, rect
.size());
129 typedef TouchSelectionControllerImpl::EditingHandleView EditingHandleView
;
131 class TouchHandleWindowTargeter
: public wm::MaskedWindowTargeter
{
133 TouchHandleWindowTargeter(aura::Window
* window
,
134 EditingHandleView
* handle_view
);
136 virtual ~TouchHandleWindowTargeter() {}
139 // wm::MaskedWindowTargeter:
140 virtual bool GetHitTestMask(aura::Window
* window
,
141 gfx::Path
* mask
) const OVERRIDE
;
143 EditingHandleView
* handle_view_
;
145 DISALLOW_COPY_AND_ASSIGN(TouchHandleWindowTargeter
);
148 // A View that displays the text selection handle.
149 class TouchSelectionControllerImpl::EditingHandleView
150 : public views::WidgetDelegateView
{
152 EditingHandleView(TouchSelectionControllerImpl
* controller
,
153 gfx::NativeView context
)
154 : controller_(controller
),
156 draw_invisible_(false) {
157 widget_
.reset(CreateTouchSelectionPopupWidget(context
, this));
158 widget_
->SetContentsView(this);
160 aura::Window
* window
= widget_
->GetNativeWindow();
161 window
->SetEventTargeter(scoped_ptr
<ui::EventTargeter
>(
162 new TouchHandleWindowTargeter(window
, this)));
164 // We are owned by the TouchSelectionController.
165 set_owned_by_client();
168 virtual ~EditingHandleView() {
169 SetWidgetVisible(false, false);
172 // Overridden from views::WidgetDelegateView:
173 virtual bool WidgetHasHitTestMask() const OVERRIDE
{
177 virtual void GetWidgetHitTestMask(gfx::Path
* mask
) const OVERRIDE
{
178 gfx::Size image_size
= GetHandleImageSize();
179 mask
->addRect(SkIntToScalar(0), SkIntToScalar(selection_rect_
.height()),
180 SkIntToScalar(image_size
.width()) + 2 * kSelectionHandleHorizPadding
,
181 SkIntToScalar(selection_rect_
.height() + image_size
.height() +
182 kSelectionHandleVertPadding
));
185 virtual void DeleteDelegate() OVERRIDE
{
186 // We are owned and deleted by TouchSelectionController.
189 // Overridden from views::View:
190 virtual void OnPaint(gfx::Canvas
* canvas
) OVERRIDE
{
193 gfx::Size image_size
= GetHandleImageSize();
194 int cursor_pos_x
= image_size
.width() / 2 - kSelectionHandleLineWidth
+
195 kSelectionHandleHorizPadding
;
197 // Draw the cursor line.
199 gfx::Rect(cursor_pos_x
, 0,
200 2 * kSelectionHandleLineWidth
+ 1, selection_rect_
.height()),
201 kSelectionHandleLineColor
);
203 // Draw the handle image.
204 canvas
->DrawImageInt(*GetHandleImage()->ToImageSkia(),
205 kSelectionHandleHorizPadding
, selection_rect_
.height());
208 virtual void OnGestureEvent(ui::GestureEvent
* event
) OVERRIDE
{
210 switch (event
->type()) {
211 case ui::ET_GESTURE_SCROLL_BEGIN
:
212 widget_
->SetCapture(this);
213 controller_
->SetDraggingHandle(this);
214 drag_offset_
= event
->y() - selection_rect_
.height() +
215 kSelectionHandleVerticalDragOffset
;
217 case ui::ET_GESTURE_SCROLL_UPDATE
: {
218 gfx::Point
drag_pos(event
->location().x(),
219 event
->location().y() - drag_offset_
);
220 controller_
->SelectionHandleDragged(drag_pos
);
223 case ui::ET_GESTURE_SCROLL_END
:
224 case ui::ET_SCROLL_FLING_START
:
225 widget_
->ReleaseCapture();
226 controller_
->SetDraggingHandle(NULL
);
233 virtual gfx::Size
GetPreferredSize() OVERRIDE
{
234 gfx::Size image_size
= GetHandleImageSize();
235 return gfx::Size(image_size
.width() + 2 * kSelectionHandleHorizPadding
,
236 image_size
.height() + selection_rect_
.height() +
237 kSelectionHandleVertPadding
);
240 bool IsWidgetVisible() const {
241 return widget_
->IsVisible();
244 void SetWidgetVisible(bool visible
, bool quick
) {
245 if (widget_
->IsVisible() == visible
)
247 wm::SetWindowVisibilityAnimationDuration(
248 widget_
->GetNativeView(),
249 base::TimeDelta::FromMilliseconds(
250 quick
? kSelectionHandleQuickFadeDurationMs
: 0));
257 void SetSelectionRectInScreen(const gfx::Rect
& rect
) {
258 gfx::Size image_size
= GetHandleImageSize();
259 selection_rect_
= rect
;
260 gfx::Rect
widget_bounds(
261 rect
.x() - image_size
.width() / 2 - kSelectionHandleHorizPadding
,
263 image_size
.width() + 2 * kSelectionHandleHorizPadding
,
264 rect
.height() + image_size
.height() + kSelectionHandleVertPadding
);
265 widget_
->SetBounds(widget_bounds
);
268 gfx::Point
GetScreenPosition() {
269 return widget_
->GetClientAreaBoundsInScreen().origin();
272 void SetDrawInvisible(bool draw_invisible
) {
273 if (draw_invisible_
== draw_invisible
)
275 draw_invisible_
= draw_invisible
;
279 const gfx::Rect
& selection_rect() const { return selection_rect_
; }
282 scoped_ptr
<Widget
> widget_
;
283 TouchSelectionControllerImpl
* controller_
;
284 gfx::Rect selection_rect_
;
286 // Vertical offset between the scroll event position and the drag position
287 // reported to the client view (see the ASCII figure at the top of the file
288 // and its description for more details).
291 // If set to true, the handle will not draw anything, hence providing an empty
292 // widget. We need this because we may want to stop showing the handle while
293 // it is being dragged. Since it is being dragged, we cannot destroy the
295 bool draw_invisible_
;
297 DISALLOW_COPY_AND_ASSIGN(EditingHandleView
);
300 TouchHandleWindowTargeter::TouchHandleWindowTargeter(
301 aura::Window
* window
,
302 EditingHandleView
* handle_view
)
303 : wm::MaskedWindowTargeter(window
),
304 handle_view_(handle_view
) {
307 bool TouchHandleWindowTargeter::GetHitTestMask(aura::Window
* window
,
308 gfx::Path
* mask
) const {
309 const gfx::Rect
& selection_rect
= handle_view_
->selection_rect();
310 gfx::Size image_size
= GetHandleImageSize();
311 mask
->addRect(SkIntToScalar(0), SkIntToScalar(selection_rect
.height()),
312 SkIntToScalar(image_size
.width()) + 2 * kSelectionHandleHorizPadding
,
313 SkIntToScalar(selection_rect
.height() + image_size
.height() +
314 kSelectionHandleVertPadding
));
318 TouchSelectionControllerImpl::TouchSelectionControllerImpl(
319 ui::TouchEditable
* client_view
)
320 : client_view_(client_view
),
321 client_widget_(NULL
),
322 selection_handle_1_(new EditingHandleView(this,
323 client_view
->GetNativeView())),
324 selection_handle_2_(new EditingHandleView(this,
325 client_view
->GetNativeView())),
326 cursor_handle_(new EditingHandleView(this,
327 client_view
->GetNativeView())),
329 dragging_handle_(NULL
) {
330 client_widget_
= Widget::GetTopLevelWidgetForNativeView(
331 client_view_
->GetNativeView());
333 client_widget_
->AddObserver(this);
334 aura::Env::GetInstance()->AddPreTargetHandler(this);
337 TouchSelectionControllerImpl::~TouchSelectionControllerImpl() {
339 aura::Env::GetInstance()->RemovePreTargetHandler(this);
341 client_widget_
->RemoveObserver(this);
344 void TouchSelectionControllerImpl::SelectionChanged() {
346 client_view_
->GetSelectionEndPoints(&r1
, &r2
);
347 gfx::Rect screen_rect_1
= ConvertToScreen(client_view_
, r1
);
348 gfx::Rect screen_rect_2
= ConvertToScreen(client_view_
, r2
);
349 gfx::Rect client_bounds
= client_view_
->GetBounds();
350 if (r1
.y() < client_bounds
.y())
351 r1
.Inset(0, client_bounds
.y() - r1
.y(), 0, 0);
352 if (r2
.y() < client_bounds
.y())
353 r2
.Inset(0, client_bounds
.y() - r2
.y(), 0, 0);
354 gfx::Rect screen_rect_1_clipped
= ConvertToScreen(client_view_
, r1
);
355 gfx::Rect screen_rect_2_clipped
= ConvertToScreen(client_view_
, r2
);
356 if (screen_rect_1_clipped
== selection_end_point_1_clipped_
&&
357 screen_rect_2_clipped
== selection_end_point_2_clipped_
)
360 selection_end_point_1_
= screen_rect_1
;
361 selection_end_point_2_
= screen_rect_2
;
362 selection_end_point_1_clipped_
= screen_rect_1_clipped
;
363 selection_end_point_2_clipped_
= screen_rect_2_clipped
;
365 if (client_view_
->DrawsHandles()) {
369 if (dragging_handle_
) {
370 // We need to reposition only the selection handle that is being dragged.
371 // The other handle stays the same. Also, the selection handle being dragged
372 // will always be at the end of selection, while the other handle will be at
374 // If the new location of this handle is out of client view, its widget
375 // should not get hidden, since it should still receive touch events.
376 // Hence, we are not using |SetHandleSelectionRect()| method here.
377 dragging_handle_
->SetSelectionRectInScreen(screen_rect_2_clipped
);
379 // Temporary fix for selection handle going outside a window. On a webpage,
380 // the page should scroll if the selection handle is dragged outside the
381 // window. That does not happen currently. So we just hide the handle for
383 // TODO(varunjain): Fix this: crbug.com/269003
384 dragging_handle_
->SetDrawInvisible(!ShouldShowHandleFor(r2
));
386 if (dragging_handle_
!= cursor_handle_
.get()) {
387 // The non-dragging-handle might have recently become visible.
388 EditingHandleView
* non_dragging_handle
= selection_handle_1_
.get();
389 if (dragging_handle_
== selection_handle_1_
) {
390 non_dragging_handle
= selection_handle_2_
.get();
391 // if handle 1 is being dragged, it is corresponding to the end of
392 // selection and the other handle to the start of selection.
393 selection_end_point_1_
= screen_rect_2
;
394 selection_end_point_2_
= screen_rect_1
;
395 selection_end_point_1_clipped_
= screen_rect_2_clipped
;
396 selection_end_point_2_clipped_
= screen_rect_1_clipped
;
398 SetHandleSelectionRect(non_dragging_handle
, r1
, screen_rect_1_clipped
);
403 // Check if there is any selection at all.
404 if (screen_rect_1
.origin() == screen_rect_2
.origin()) {
405 selection_handle_1_
->SetWidgetVisible(false, false);
406 selection_handle_2_
->SetWidgetVisible(false, false);
407 SetHandleSelectionRect(cursor_handle_
.get(), r1
, screen_rect_1_clipped
);
411 cursor_handle_
->SetWidgetVisible(false, false);
412 SetHandleSelectionRect(selection_handle_1_
.get(), r1
,
413 screen_rect_1_clipped
);
414 SetHandleSelectionRect(selection_handle_2_
.get(), r2
,
415 screen_rect_2_clipped
);
419 bool TouchSelectionControllerImpl::IsHandleDragInProgress() {
420 return !!dragging_handle_
;
423 void TouchSelectionControllerImpl::HideHandles(bool quick
) {
424 selection_handle_1_
->SetWidgetVisible(false, quick
);
425 selection_handle_2_
->SetWidgetVisible(false, quick
);
426 cursor_handle_
->SetWidgetVisible(false, quick
);
429 void TouchSelectionControllerImpl::SetDraggingHandle(
430 EditingHandleView
* handle
) {
431 dragging_handle_
= handle
;
432 if (dragging_handle_
)
435 StartContextMenuTimer();
438 void TouchSelectionControllerImpl::SelectionHandleDragged(
439 const gfx::Point
& drag_pos
) {
440 // We do not want to show the context menu while dragging.
443 DCHECK(dragging_handle_
);
444 gfx::Point drag_pos_in_client
= drag_pos
;
445 ConvertPointToClientView(dragging_handle_
, &drag_pos_in_client
);
447 if (dragging_handle_
== cursor_handle_
.get()) {
448 client_view_
->MoveCaretTo(drag_pos_in_client
);
452 // Find the stationary selection handle.
453 gfx::Rect fixed_handle_rect
= selection_end_point_1_
;
454 if (selection_handle_1_
== dragging_handle_
)
455 fixed_handle_rect
= selection_end_point_2_
;
457 // Find selection end points in client_view's coordinate system.
458 gfx::Point p2
= fixed_handle_rect
.origin();
459 p2
.Offset(0, fixed_handle_rect
.height() / 2);
460 client_view_
->ConvertPointFromScreen(&p2
);
462 // Instruct client_view to select the region between p1 and p2. The position
463 // of |fixed_handle| is the start and that of |dragging_handle| is the end
465 client_view_
->SelectRect(p2
, drag_pos_in_client
);
468 void TouchSelectionControllerImpl::ConvertPointToClientView(
469 EditingHandleView
* source
, gfx::Point
* point
) {
470 View::ConvertPointToScreen(source
, point
);
471 client_view_
->ConvertPointFromScreen(point
);
474 void TouchSelectionControllerImpl::SetHandleSelectionRect(
475 EditingHandleView
* handle
,
476 const gfx::Rect
& rect
,
477 const gfx::Rect
& rect_in_screen
) {
478 handle
->SetWidgetVisible(ShouldShowHandleFor(rect
), false);
479 if (handle
->IsWidgetVisible())
480 handle
->SetSelectionRectInScreen(rect_in_screen
);
483 bool TouchSelectionControllerImpl::ShouldShowHandleFor(
484 const gfx::Rect
& rect
) const {
485 if (rect
.height() < kSelectionHandleBarMinHeight
)
487 gfx::Rect bounds
= client_view_
->GetBounds();
488 bounds
.Inset(0, 0, 0, -kSelectionHandleBarBottomAllowance
);
489 return bounds
.Contains(rect
);
492 bool TouchSelectionControllerImpl::IsCommandIdEnabled(int command_id
) const {
493 return client_view_
->IsCommandIdEnabled(command_id
);
496 void TouchSelectionControllerImpl::ExecuteCommand(int command_id
,
499 client_view_
->ExecuteCommand(command_id
, event_flags
);
502 void TouchSelectionControllerImpl::OpenContextMenu() {
503 // Context menu should appear centered on top of the selected region.
504 const gfx::Rect rect
= context_menu_
->GetAnchorRect();
505 const gfx::Point
anchor(rect
.CenterPoint().x(), rect
.y());
507 client_view_
->OpenContextMenu(anchor
);
510 void TouchSelectionControllerImpl::OnMenuClosed(TouchEditingMenuView
* menu
) {
511 if (menu
== context_menu_
)
512 context_menu_
= NULL
;
515 void TouchSelectionControllerImpl::OnWidgetClosing(Widget
* widget
) {
516 DCHECK_EQ(client_widget_
, widget
);
517 client_widget_
= NULL
;
520 void TouchSelectionControllerImpl::OnWidgetBoundsChanged(
522 const gfx::Rect
& new_bounds
) {
523 DCHECK_EQ(client_widget_
, widget
);
528 void TouchSelectionControllerImpl::OnKeyEvent(ui::KeyEvent
* event
) {
529 client_view_
->DestroyTouchSelection();
532 void TouchSelectionControllerImpl::OnMouseEvent(ui::MouseEvent
* event
) {
533 aura::client::CursorClient
* cursor_client
= aura::client::GetCursorClient(
534 client_view_
->GetNativeView()->GetRootWindow());
535 if (!cursor_client
|| cursor_client
->IsMouseEventsEnabled())
536 client_view_
->DestroyTouchSelection();
539 void TouchSelectionControllerImpl::OnScrollEvent(ui::ScrollEvent
* event
) {
540 client_view_
->DestroyTouchSelection();
543 void TouchSelectionControllerImpl::ContextMenuTimerFired() {
544 // Get selection end points in client_view's space.
545 gfx::Rect end_rect_1_in_screen
;
546 gfx::Rect end_rect_2_in_screen
;
547 if (cursor_handle_
->IsWidgetVisible()) {
548 end_rect_1_in_screen
= selection_end_point_1_clipped_
;
549 end_rect_2_in_screen
= end_rect_1_in_screen
;
551 end_rect_1_in_screen
= selection_end_point_1_clipped_
;
552 end_rect_2_in_screen
= selection_end_point_2_clipped_
;
555 // Convert from screen to client.
556 gfx::Rect
end_rect_1(ConvertFromScreen(client_view_
, end_rect_1_in_screen
));
557 gfx::Rect
end_rect_2(ConvertFromScreen(client_view_
, end_rect_2_in_screen
));
559 // if selection is completely inside the view, we display the context menu
560 // in the middle of the end points on the top. Else, we show it above the
561 // visible handle. If no handle is visible, we do not show the menu.
562 gfx::Rect menu_anchor
;
563 if (ShouldShowHandleFor(end_rect_1
) &&
564 ShouldShowHandleFor(end_rect_2
))
565 menu_anchor
= Union(end_rect_1_in_screen
,end_rect_2_in_screen
);
566 else if (ShouldShowHandleFor(end_rect_1
))
567 menu_anchor
= end_rect_1_in_screen
;
568 else if (ShouldShowHandleFor(end_rect_2
))
569 menu_anchor
= end_rect_2_in_screen
;
573 DCHECK(!context_menu_
);
574 context_menu_
= TouchEditingMenuView::Create(this, menu_anchor
,
575 GetHandleImageSize(),
576 client_view_
->GetNativeView());
579 void TouchSelectionControllerImpl::StartContextMenuTimer() {
580 if (context_menu_timer_
.IsRunning())
582 context_menu_timer_
.Start(
584 base::TimeDelta::FromMilliseconds(kContextMenuTimoutMs
),
586 &TouchSelectionControllerImpl::ContextMenuTimerFired
);
589 void TouchSelectionControllerImpl::UpdateContextMenu() {
590 // Hide context menu to be shown when the timer fires.
592 StartContextMenuTimer();
595 void TouchSelectionControllerImpl::HideContextMenu() {
597 context_menu_
->Close();
598 context_menu_
= NULL
;
599 context_menu_timer_
.Stop();
602 gfx::NativeView
TouchSelectionControllerImpl::GetCursorHandleNativeView() {
603 return cursor_handle_
->GetWidget()->GetNativeView();
606 gfx::Point
TouchSelectionControllerImpl::GetSelectionHandle1Position() {
607 return selection_handle_1_
->GetScreenPosition();
610 gfx::Point
TouchSelectionControllerImpl::GetSelectionHandle2Position() {
611 return selection_handle_2_
->GetScreenPosition();
614 gfx::Point
TouchSelectionControllerImpl::GetCursorHandlePosition() {
615 return cursor_handle_
->GetScreenPosition();
618 bool TouchSelectionControllerImpl::IsSelectionHandle1Visible() {
619 return selection_handle_1_
->IsWidgetVisible();
622 bool TouchSelectionControllerImpl::IsSelectionHandle2Visible() {
623 return selection_handle_2_
->IsWidgetVisible();
626 bool TouchSelectionControllerImpl::IsCursorHandleVisible() {
627 return cursor_handle_
->IsWidgetVisible();