blob: 2b74121c1e464029c4056b7282d2980ebf9a335a [file] [log] [blame]
// Copyright (c) 2012 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/controls/slider.h"
#include "base/logging.h"
#include "base/message_loop/message_loop.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "grit/ui_resources.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkPaint.h"
#include "ui/base/accessibility/accessible_view_state.h"
#include "ui/base/animation/slide_animation.h"
#include "ui/base/events/event.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/point.h"
#include "ui/gfx/rect.h"
#include "ui/views/widget/widget.h"
namespace {
const int kSlideValueChangeDurationMS = 150;
const int kBarImagesActive[] = {
IDR_SLIDER_ACTIVE_LEFT,
IDR_SLIDER_ACTIVE_CENTER,
IDR_SLIDER_PRESSED_CENTER,
IDR_SLIDER_PRESSED_RIGHT,
};
const int kBarImagesDisabled[] = {
IDR_SLIDER_DISABLED_LEFT,
IDR_SLIDER_DISABLED_CENTER,
IDR_SLIDER_DISABLED_CENTER,
IDR_SLIDER_DISABLED_RIGHT,
};
// The image chunks.
enum BorderElements {
LEFT,
CENTER_LEFT,
CENTER_RIGHT,
RIGHT,
};
}
namespace views {
Slider::Slider(SliderListener* listener, Orientation orientation)
: listener_(listener),
orientation_(orientation),
value_(0.f),
keyboard_increment_(0.1f),
animating_value_(0.f),
value_is_valid_(false),
accessibility_events_enabled_(true),
focus_border_color_(0),
bar_active_images_(kBarImagesActive),
bar_disabled_images_(kBarImagesDisabled) {
EnableCanvasFlippingForRTLUI(true);
set_focusable(true);
UpdateState(true);
}
Slider::~Slider() {
}
void Slider::SetValue(float value) {
SetValueInternal(value, VALUE_CHANGED_BY_API);
}
void Slider::SetKeyboardIncrement(float increment) {
keyboard_increment_ = increment;
}
void Slider::SetValueInternal(float value, SliderChangeReason reason) {
bool old_value_valid = value_is_valid_;
value_is_valid_ = true;
if (value < 0.0)
value = 0.0;
else if (value > 1.0)
value = 1.0;
if (value_ == value)
return;
float old_value = value_;
value_ = value;
if (listener_)
listener_->SliderValueChanged(this, value_, old_value, reason);
if (old_value_valid && base::MessageLoop::current()) {
// Do not animate when setting the value of the slider for the first time.
// There is no message-loop when running tests. So we cannot animate then.
animating_value_ = old_value;
move_animation_.reset(new ui::SlideAnimation(this));
move_animation_->SetSlideDuration(kSlideValueChangeDurationMS);
move_animation_->Show();
AnimationProgressed(move_animation_.get());
} else {
SchedulePaint();
}
if (accessibility_events_enabled_ && GetWidget()) {
NotifyAccessibilityEvent(
ui::AccessibilityTypes::EVENT_VALUE_CHANGED, true);
}
}
void Slider::PrepareForMove(const gfx::Point& point) {
// Try to remember the position of the mouse cursor on the button.
gfx::Insets inset = GetInsets();
gfx::Rect content = GetContentsBounds();
float value = move_animation_.get() && move_animation_->is_animating() ?
animating_value_ : value_;
// For the horizontal orientation.
const int thumb_x = value * (content.width() - thumb_->width());
const int candidate_x = (base::i18n::IsRTL() ?
width() - (point.x() - inset.left()) :
point.x() - inset.left()) - thumb_x;
if (candidate_x >= 0 && candidate_x < thumb_->width())
initial_button_offset_.set_x(candidate_x);
else
initial_button_offset_.set_x(thumb_->width() / 2);
// For the vertical orientation.
const int thumb_y = (1.0 - value) * (content.height() - thumb_->height());
const int candidate_y = point.y() - thumb_y;
if (candidate_y >= 0 && candidate_y < thumb_->height())
initial_button_offset_.set_y(candidate_y);
else
initial_button_offset_.set_y(thumb_->height() / 2);
}
void Slider::MoveButtonTo(const gfx::Point& point) {
gfx::Insets inset = GetInsets();
// Calculate the value.
if (orientation_ == HORIZONTAL) {
int amount = base::i18n::IsRTL() ?
width() - inset.left() - point.x() - initial_button_offset_.x() :
point.x() - inset.left() - initial_button_offset_.x();
SetValueInternal(static_cast<float>(amount) /
(width() - inset.width() - thumb_->width()),
VALUE_CHANGED_BY_USER);
} else {
SetValueInternal(
1.0f - static_cast<float>(point.y() - initial_button_offset_.y()) /
(height() - thumb_->height()),
VALUE_CHANGED_BY_USER);
}
}
void Slider::UpdateState(bool control_on) {
ResourceBundle& rb = ResourceBundle::GetSharedInstance();
if (control_on) {
thumb_ = rb.GetImageNamed(IDR_SLIDER_ACTIVE_THUMB).ToImageSkia();
for (int i = 0; i < 4; ++i)
images_[i] = rb.GetImageNamed(bar_active_images_[i]).ToImageSkia();
} else {
thumb_ = rb.GetImageNamed(IDR_SLIDER_DISABLED_THUMB).ToImageSkia();
for (int i = 0; i < 4; ++i)
images_[i] = rb.GetImageNamed(bar_disabled_images_[i]).ToImageSkia();
}
bar_height_ = images_[LEFT]->height();
SchedulePaint();
}
void Slider::SetAccessibleName(const string16& name) {
accessible_name_ = name;
}
gfx::Size Slider::GetPreferredSize() {
const int kSizeMajor = 200;
const int kSizeMinor = 40;
if (orientation_ == HORIZONTAL)
return gfx::Size(std::max(width(), kSizeMajor), kSizeMinor);
return gfx::Size(kSizeMinor, std::max(height(), kSizeMajor));
}
void Slider::OnPaint(gfx::Canvas* canvas) {
gfx::Rect content = GetContentsBounds();
float value = move_animation_.get() && move_animation_->is_animating() ?
animating_value_ : value_;
if (orientation_ == HORIZONTAL) {
// Paint slider bar with image resources.
// Inset the slider bar a little bit, so that the left or the right end of
// the slider bar will not be exposed under the thumb button when the thumb
// button slides to the left most or right most position.
const int kBarInsetX = 2;
int bar_width = content.width() - kBarInsetX * 2;
int bar_cy = content.height() / 2 - bar_height_ / 2;
int w = content.width() - thumb_->width();
int full = value * w;
int middle = std::max(full, images_[LEFT]->width());
canvas->Save();
canvas->Translate(gfx::Vector2d(kBarInsetX, bar_cy));
canvas->DrawImageInt(*images_[LEFT], 0, 0);
canvas->DrawImageInt(*images_[RIGHT],
bar_width - images_[RIGHT]->width(),
0);
canvas->TileImageInt(*images_[CENTER_LEFT],
images_[LEFT]->width(),
0,
middle - images_[LEFT]->width(),
bar_height_);
canvas->TileImageInt(*images_[CENTER_RIGHT],
middle,
0,
bar_width - middle - images_[RIGHT]->width(),
bar_height_);
canvas->Restore();
// Paint slider thumb.
int button_cx = content.x() + full;
int thumb_y = content.height() / 2 - thumb_->height() / 2;
canvas->DrawImageInt(*thumb_, button_cx, thumb_y);
} else {
// TODO(jennyz): draw vertical slider bar with resources.
// TODO(sad): The painting code should use NativeTheme for various
// platforms.
const int kButtonRadius = thumb_->width() / 2;
const int kLineThickness = bar_height_ / 2;
const SkColor kFullColor = SkColorSetARGB(125, 0, 0, 0);
const SkColor kEmptyColor = SkColorSetARGB(50, 0, 0, 0);
int h = content.height() - thumb_->height();
int full = value * h;
int empty = h - full;
int x = content.width() / 2 - kLineThickness / 2;
canvas->FillRect(gfx::Rect(x, content.y() + kButtonRadius,
kLineThickness, empty),
kEmptyColor);
canvas->FillRect(gfx::Rect(x, content.y() + empty + 2 * kButtonRadius,
kLineThickness, full),
kFullColor);
// TODO(mtomasz): We draw a thumb here because so far it is the same
// for horizontal and vertical orientations. If it is different, then
// we will need a separate resource.
int button_cy = content.y() + h - full;
int thumb_x = content.width() / 2 - thumb_->width() / 2;
canvas->DrawImageInt(*thumb_, thumb_x, button_cy);
}
View::OnPaint(canvas);
}
bool Slider::OnMousePressed(const ui::MouseEvent& event) {
if (!event.IsOnlyLeftMouseButton())
return false;
if (listener_)
listener_->SliderDragStarted(this);
PrepareForMove(event.location());
MoveButtonTo(event.location());
return true;
}
bool Slider::OnMouseDragged(const ui::MouseEvent& event) {
MoveButtonTo(event.location());
return true;
}
void Slider::OnMouseReleased(const ui::MouseEvent& event) {
if (listener_)
listener_->SliderDragEnded(this);
}
bool Slider::OnKeyPressed(const ui::KeyEvent& event) {
if (orientation_ == HORIZONTAL) {
if (event.key_code() == ui::VKEY_LEFT) {
SetValueInternal(value_ - keyboard_increment_, VALUE_CHANGED_BY_USER);
return true;
} else if (event.key_code() == ui::VKEY_RIGHT) {
SetValueInternal(value_ + keyboard_increment_, VALUE_CHANGED_BY_USER);
return true;
}
} else {
if (event.key_code() == ui::VKEY_DOWN) {
SetValueInternal(value_ - keyboard_increment_, VALUE_CHANGED_BY_USER);
return true;
} else if (event.key_code() == ui::VKEY_UP) {
SetValueInternal(value_ + keyboard_increment_, VALUE_CHANGED_BY_USER);
return true;
}
}
return false;
}
void Slider::OnGestureEvent(ui::GestureEvent* event) {
if (event->type() == ui::ET_GESTURE_SCROLL_BEGIN ||
event->type() == ui::ET_GESTURE_TAP_DOWN) {
PrepareForMove(event->location());
MoveButtonTo(event->location());
event->SetHandled();
} else if (event->type() == ui::ET_GESTURE_SCROLL_UPDATE ||
event->type() == ui::ET_GESTURE_SCROLL_END) {
MoveButtonTo(event->location());
event->SetHandled();
}
}
void Slider::AnimationProgressed(const ui::Animation* animation) {
animating_value_ = animation->CurrentValueBetween(animating_value_, value_);
SchedulePaint();
}
void Slider::GetAccessibleState(ui::AccessibleViewState* state) {
state->role = ui::AccessibilityTypes::ROLE_SLIDER;
state->name = accessible_name_;
state->value = UTF8ToUTF16(
base::StringPrintf("%d%%", (int)(value_ * 100 + 0.5)));
}
void Slider::OnPaintFocusBorder(gfx::Canvas* canvas) {
if (!focus_border_color_) {
View::OnPaintFocusBorder(canvas);
} else if (HasFocus() && (focusable() || IsAccessibilityFocusable())) {
canvas->DrawRect(gfx::Rect(1, 1, width() - 3, height() - 3),
focus_border_color_);
}
}
} // namespace views