blob: 6ce527efe6353d7572b501c2aef92feea1216d2a [file] [log] [blame]
// Copyright (c) 2011 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 "views/touchui/touch_selection_controller_impl.h"
#include "base/utf_string_conversions.h"
#include "base/time.h"
#include "grit/ui_strings.h"
#include "third_party/skia/include/effects/SkGradientShader.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/canvas_skia.h"
#include "ui/gfx/path.h"
#include "ui/gfx/rect.h"
#include "ui/gfx/screen.h"
#include "ui/gfx/size.h"
#include "ui/gfx/transform.h"
#include "views/background.h"
#include "views/controls/button/button.h"
#include "views/controls/button/custom_button.h"
#include "views/controls/button/text_button.h"
#include "views/controls/menu/menu_config.h"
#include "views/controls/label.h"
#include "views/layout/box_layout.h"
#include "views/widget/widget.h"
namespace {
// Constants defining the visual attributes of selection handles
const int kSelectionHandleRadius = 10;
const int kSelectionHandleCursorHeight = 10;
const int kSelectionHandleAlpha = 0x7F;
const SkColor kSelectionHandleColor =
SkColorSetA(SK_ColorBLUE, kSelectionHandleAlpha);
// The minimum selection size to trigger selection controller.
const int kMinSelectionSize = 4;
const int kContextMenuCommands[] = {IDS_APP_CUT,
IDS_APP_COPY,
// TODO(varunjain): PASTE is acting funny due to some gtk clipboard issue.
// Uncomment the following when that is fixed.
// IDS_APP_PASTE,
IDS_APP_DELETE,
IDS_APP_SELECT_ALL};
const int kContextMenuPadding = 2;
const int kContextMenuTimoutMs = 1000;
const int kContextMenuVerticalOffset = 25;
// Convenience struct to represent a circle shape.
struct Circle {
int radius;
gfx::Point center;
SkColor color;
};
// Creates a widget to host SelectionHandleView.
views::Widget* CreateTouchSelectionPopupWidget() {
views::Widget* widget = new views::Widget;
views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
params.can_activate = false;
params.transparent = true;
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
widget->Init(params);
return widget;
}
void PaintCircle(const Circle& circle, gfx::Canvas* canvas) {
SkPaint paint;
paint.setAntiAlias(true);
paint.setStyle(SkPaint::kFill_Style);
paint.setColor(circle.color);
gfx::Path path;
gfx::Rect bounds(circle.center.x() - circle.radius,
circle.center.y() - circle.radius,
circle.radius * 2,
circle.radius * 2);
SkRect rect;
rect.set(SkIntToScalar(bounds.x()), SkIntToScalar(bounds.y()),
SkIntToScalar(bounds.right()), SkIntToScalar(bounds.bottom()));
SkScalar radius = SkIntToScalar(circle.radius);
path.addRoundRect(rect, radius, radius);
canvas->GetSkCanvas()->drawPath(path, paint);
}
// The points may not match exactly, since the selection range computation may
// introduce some floating point errors. So check for a minimum size to decide
// whether or not there is any selection.
bool IsEmptySelection(const gfx::Point& p1, const gfx::Point& p2) {
int delta_x = p2.x() - p1.x();
int delta_y = p2.y() - p1.y();
return (abs(delta_x) < kMinSelectionSize && abs(delta_y) < kMinSelectionSize);
}
} // namespace
namespace views {
// A View that displays the text selection handle.
class TouchSelectionControllerImpl::SelectionHandleView : public View {
public:
SelectionHandleView(TouchSelectionControllerImpl* controller)
: controller_(controller) {
widget_.reset(CreateTouchSelectionPopupWidget());
widget_->SetContentsView(this);
widget_->SetAlwaysOnTop(true);
// We are owned by the TouchSelectionController.
set_parent_owned(false);
}
virtual ~SelectionHandleView() {
}
virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE {
Circle circle = {kSelectionHandleRadius, gfx::Point(kSelectionHandleRadius,
kSelectionHandleRadius + kSelectionHandleCursorHeight),
kSelectionHandleColor};
PaintCircle(circle, canvas);
canvas->DrawLineInt(kSelectionHandleColor, kSelectionHandleRadius, 0,
kSelectionHandleRadius, kSelectionHandleCursorHeight);
}
virtual bool OnMousePressed(const MouseEvent& event) OVERRIDE {
controller_->dragging_handle_ = this;
return true;
}
virtual bool OnMouseDragged(const MouseEvent& event) OVERRIDE {
controller_->SelectionHandleDragged(event.location());
return true;
}
virtual void OnMouseReleased(const MouseEvent& event) OVERRIDE {
controller_->dragging_handle_ = NULL;
}
virtual void OnMouseCaptureLost() OVERRIDE {
controller_->dragging_handle_ = NULL;
}
virtual void SetVisible(bool visible) OVERRIDE {
// We simply show/hide the container widget.
if (visible != widget_->IsVisible()) {
if (visible)
widget_->Show();
else
widget_->Hide();
}
View::SetVisible(visible);
}
virtual gfx::Size GetPreferredSize() OVERRIDE {
return gfx::Size(2 * kSelectionHandleRadius,
2 * kSelectionHandleRadius + kSelectionHandleCursorHeight);
}
void SetScreenPosition(const gfx::Point& position) {
gfx::Rect widget_bounds(position.x() - kSelectionHandleRadius, position.y(),
2 * kSelectionHandleRadius,
2 * kSelectionHandleRadius + kSelectionHandleCursorHeight);
widget_->SetBounds(widget_bounds);
}
gfx::Point GetScreenPosition() {
return widget_->GetClientAreaScreenBounds().origin();
}
private:
scoped_ptr<Widget> widget_;
TouchSelectionControllerImpl* controller_;
DISALLOW_COPY_AND_ASSIGN(SelectionHandleView);
};
class ContextMenuButtonBackground : public Background {
public:
ContextMenuButtonBackground() {}
virtual void Paint(gfx::Canvas* canvas, View* view) const OVERRIDE {
CustomButton::ButtonState state = static_cast<CustomButton*>(view)->state();
SkColor background_color, border_color;
if (state == CustomButton::BS_NORMAL) {
background_color = SkColorSetARGB(102, 255, 255, 255);
border_color = SkColorSetARGB(36, 0, 0, 0);
} else {
background_color = SkColorSetARGB(13, 0, 0, 0);
border_color = SkColorSetARGB(72, 0, 0, 0);
}
int w = view->width();
int h = view->height();
canvas->FillRectInt(background_color, 1, 1, w - 2, h - 2);
canvas->FillRectInt(border_color, 2, 0, w - 4, 1);
canvas->FillRectInt(border_color, 1, 1, 1, 1);
canvas->FillRectInt(border_color, 0, 2, 1, h - 4);
canvas->FillRectInt(border_color, 1, h - 2, 1, 1);
canvas->FillRectInt(border_color, 2, h - 1, w - 4, 1);
canvas->FillRectInt(border_color, w - 2, 1, 1, 1);
canvas->FillRectInt(border_color, w - 1, 2, 1, h - 4);
canvas->FillRectInt(border_color, w - 2, h - 2, 1, 1);
}
private:
DISALLOW_COPY_AND_ASSIGN(ContextMenuButtonBackground);
};
// A View that displays the touch context menu.
class TouchSelectionControllerImpl::TouchContextMenuView
: public ButtonListener,
public View {
public:
TouchContextMenuView(TouchSelectionControllerImpl* controller)
: controller_(controller) {
widget_.reset(CreateTouchSelectionPopupWidget());
widget_->SetContentsView(this);
widget_->SetAlwaysOnTop(true);
// We are owned by the TouchSelectionController.
set_parent_owned(false);
SetLayoutManager(new BoxLayout(BoxLayout::kHorizontal, kContextMenuPadding,
kContextMenuPadding, kContextMenuPadding));
}
virtual ~TouchContextMenuView() {
}
virtual void SetVisible(bool visible) OVERRIDE {
// We simply show/hide the container widget.
if (visible != widget_->IsVisible()) {
if (visible)
widget_->Show();
else
widget_->Hide();
}
View::SetVisible(visible);
}
void SetScreenPosition(const gfx::Point& position) {
RefreshButtonsAndSetWidgetPosition(position);
}
gfx::Point GetScreenPosition() {
return widget_->GetClientAreaScreenBounds().origin();
}
void OnPaintBackground(gfx::Canvas* canvas) OVERRIDE {
// TODO(varunjain): the following color scheme is copied from
// menu_scroll_view_container.cc. Figure out how to consolidate the two
// pieces of code.
#if defined(OS_CHROMEOS)
static const SkColor kGradientColors[2] = {
SK_ColorWHITE,
SkColorSetRGB(0xF0, 0xF0, 0xF0)
};
static const SkScalar kGradientPoints[2] = {
SkIntToScalar(0),
SkIntToScalar(1)
};
SkPoint points[2];
points[0].set(SkIntToScalar(0), SkIntToScalar(0));
points[1].set(SkIntToScalar(0), SkIntToScalar(height()));
SkShader* shader = SkGradientShader::CreateLinear(points,
kGradientColors, kGradientPoints, arraysize(kGradientPoints),
SkShader::kRepeat_TileMode);
DCHECK(shader);
SkPaint paint;
paint.setShader(shader);
shader->unref();
paint.setStyle(SkPaint::kFill_Style);
paint.setXfermodeMode(SkXfermode::kSrc_Mode);
canvas->DrawRectInt(0, 0, width(), height(), paint);
#else
// This is the same as COLOR_TOOLBAR.
canvas->GetSkCanvas()->drawColor(SkColorSetRGB(210, 225, 246),
SkXfermode::kSrc_Mode);
#endif
}
// ButtonListener
virtual void ButtonPressed(Button* sender, const views::Event& event) {
controller_->ExecuteCommand(sender->tag());
}
private:
// Queries the client view for what elements to show in the menu and sizes
// the menu appropriately.
void RefreshButtonsAndSetWidgetPosition(const gfx::Point& position) {
RemoveAllChildViews(true);
int total_width = 0;
int height = 0;
for (size_t i = 0; i < arraysize(kContextMenuCommands); i++) {
int command_id = kContextMenuCommands[i];
if (controller_->IsCommandIdEnabled(command_id)) {
TextButton* button = new TextButton(
this, l10n_util::GetStringUTF16(command_id));
button->set_focusable(true);
button->set_request_focus_on_press(false);
button->set_prefix_type(TextButton::PREFIX_HIDE);
button->SetEnabledColor(MenuConfig::instance().text_color);
button->set_background(new ContextMenuButtonBackground());
button->set_alignment(TextButton::ALIGN_CENTER);
button->SetFont(ui::ResourceBundle::GetSharedInstance().GetFont(
ui::ResourceBundle::LargeFont));
button->set_tag(command_id);
AddChildView(button);
gfx::Size button_size = button->GetPreferredSize();
total_width += button_size.width() + kContextMenuPadding;
if (height < button_size.height())
height = button_size.height();
}
}
gfx::Rect widget_bounds(position.x() - total_width / 2,
position.y() - height,
total_width,
height);
gfx::Rect monitor_bounds =
gfx::Screen::GetMonitorAreaNearestPoint(position);
widget_->SetBounds(widget_bounds.AdjustToFit(monitor_bounds));
Layout();
}
scoped_ptr<Widget> widget_;
TouchSelectionControllerImpl* controller_;
DISALLOW_COPY_AND_ASSIGN(TouchContextMenuView);
};
TouchSelectionControllerImpl::TouchSelectionControllerImpl(
TouchSelectionClientView* client_view)
: client_view_(client_view),
selection_handle_1_(new SelectionHandleView(this)),
selection_handle_2_(new SelectionHandleView(this)),
context_menu_(new TouchContextMenuView(this)),
dragging_handle_(NULL) {
}
TouchSelectionControllerImpl::~TouchSelectionControllerImpl() {
}
void TouchSelectionControllerImpl::SelectionChanged(const gfx::Point& p1,
const gfx::Point& p2) {
gfx::Point screen_pos_1(p1);
View::ConvertPointToScreen(client_view_, &screen_pos_1);
gfx::Point screen_pos_2(p2);
View::ConvertPointToScreen(client_view_, &screen_pos_2);
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.
dragging_handle_->SetScreenPosition(screen_pos_2);
} else {
UpdateContextMenu(p1, p2);
// Check if there is any selection at all.
if (IsEmptySelection(screen_pos_2, screen_pos_1)) {
selection_handle_1_->SetVisible(false);
selection_handle_2_->SetVisible(false);
return;
}
if (client_view_->bounds().Contains(p1)) {
selection_handle_1_->SetScreenPosition(screen_pos_1);
selection_handle_1_->SetVisible(true);
} else {
selection_handle_1_->SetVisible(false);
}
if (client_view_->bounds().Contains(p2)) {
selection_handle_2_->SetScreenPosition(screen_pos_2);
selection_handle_2_->SetVisible(true);
} else {
selection_handle_2_->SetVisible(false);
}
}
}
void TouchSelectionControllerImpl::ClientViewLostFocus() {
selection_handle_1_->SetVisible(false);
selection_handle_2_->SetVisible(false);
HideContextMenu();
}
void TouchSelectionControllerImpl::SelectionHandleDragged(
const gfx::Point& drag_pos) {
// We do not want to show the context menu while dragging.
HideContextMenu();
context_menu_timer_.Start(
FROM_HERE,
base::TimeDelta::FromMilliseconds(kContextMenuTimoutMs),
this,
&TouchSelectionControllerImpl::ContextMenuTimerFired);
if (client_view_->GetWidget()) {
DCHECK(dragging_handle_);
// Find the stationary selection handle.
SelectionHandleView* fixed_handle = selection_handle_1_.get();
if (fixed_handle == dragging_handle_)
fixed_handle = selection_handle_2_.get();
// Find selection end points in client_view's coordinate system.
gfx::Point p1(drag_pos.x() + kSelectionHandleRadius, drag_pos.y());
ConvertPointToClientView(dragging_handle_, &p1);
gfx::Point p2(kSelectionHandleRadius, 0);
ConvertPointToClientView(fixed_handle, &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, p1);
}
}
void TouchSelectionControllerImpl::ConvertPointToClientView(
SelectionHandleView* source, gfx::Point* point) {
View::ConvertPointToScreen(source, point);
gfx::Rect r = client_view_->GetWidget()->GetClientAreaScreenBounds();
point->SetPoint(point->x() - r.x(), point->y() - r.y());
View::ConvertPointFromWidget(client_view_, point);
}
bool TouchSelectionControllerImpl::IsCommandIdEnabled(int command_id) const {
return client_view_->IsCommandIdEnabled(command_id);
}
void TouchSelectionControllerImpl::ExecuteCommand(int command_id) {
HideContextMenu();
client_view_->ExecuteCommand(command_id);
}
void TouchSelectionControllerImpl::ContextMenuTimerFired() {
// Get selection end points in client_view's space.
gfx::Point p1(kSelectionHandleRadius, 0);
ConvertPointToClientView(selection_handle_1_.get(), &p1);
gfx::Point p2(kSelectionHandleRadius, 0);
ConvertPointToClientView(selection_handle_2_.get(), &p2);
// 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 the menu on the
// top border of the view in the center.
gfx::Point menu_pos;
if (client_view_->bounds().Contains(p1) &&
client_view_->bounds().Contains(p2)) {
menu_pos.set_x((p1.x() + p2.x()) / 2);
menu_pos.set_y(std::min(p1.y(), p2.y()) - kContextMenuVerticalOffset);
} else {
menu_pos.set_x(client_view_->x() + client_view_->width() / 2);
menu_pos.set_y(client_view_->y());
}
View::ConvertPointToScreen(client_view_, &menu_pos);
context_menu_->SetScreenPosition(menu_pos);
context_menu_->SetVisible(true);
}
void TouchSelectionControllerImpl::UpdateContextMenu(const gfx::Point& p1,
const gfx::Point& p2) {
// Hide context menu to be shown when the timer fires.
HideContextMenu();
// If there is selection, we restart the context menu timer.
if (!IsEmptySelection(p1, p2)) {
context_menu_timer_.Start(
FROM_HERE,
base::TimeDelta::FromMilliseconds(kContextMenuTimoutMs),
this,
&TouchSelectionControllerImpl::ContextMenuTimerFired);
}
}
void TouchSelectionControllerImpl::HideContextMenu() {
context_menu_->SetVisible(false);
context_menu_timer_.Stop();
}
gfx::Point TouchSelectionControllerImpl::GetSelectionHandle1Position() {
return selection_handle_1_->GetScreenPosition();
}
gfx::Point TouchSelectionControllerImpl::GetSelectionHandle2Position() {
return selection_handle_2_->GetScreenPosition();
}
bool TouchSelectionControllerImpl::IsSelectionHandle1Visible() {
return selection_handle_1_->IsVisible();
}
bool TouchSelectionControllerImpl::IsSelectionHandle2Visible() {
return selection_handle_2_->IsVisible();
}
TouchSelectionController* TouchSelectionController::create(
TouchSelectionClientView* client_view) {
return new TouchSelectionControllerImpl(client_view);
}
} // namespace views