slider.cc revision 5821806d5e7f356e8fa4b058a389a808ea183019
1// Copyright (c) 2012 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.
4
5#include "ui/views/controls/slider.h"
6
7#include "base/logging.h"
8#include "base/message_loop.h"
9#include "base/stringprintf.h"
10#include "base/utf_string_conversions.h"
11#include "grit/ui_resources.h"
12#include "third_party/skia/include/core/SkCanvas.h"
13#include "third_party/skia/include/core/SkColor.h"
14#include "third_party/skia/include/core/SkPaint.h"
15#include "ui/base/accessibility/accessible_view_state.h"
16#include "ui/base/animation/slide_animation.h"
17#include "ui/base/events/event.h"
18#include "ui/base/resource/resource_bundle.h"
19#include "ui/gfx/canvas.h"
20#include "ui/gfx/point.h"
21#include "ui/gfx/rect.h"
22#include "ui/views/widget/widget.h"
23
24namespace {
25const int kSlideValueChangeDurationMS = 150;
26
27const int kBarImagesActive[] = {
28    IDR_SLIDER_ACTIVE_LEFT,
29    IDR_SLIDER_ACTIVE_CENTER,
30    IDR_SLIDER_PRESSED_CENTER,
31    IDR_SLIDER_PRESSED_RIGHT,
32};
33
34const int kBarImagesDisabled[] = {
35    IDR_SLIDER_DISABLED_LEFT,
36    IDR_SLIDER_DISABLED_CENTER,
37    IDR_SLIDER_DISABLED_CENTER,
38    IDR_SLIDER_DISABLED_RIGHT,
39};
40
41// The image chunks.
42enum BorderElements {
43  LEFT,
44  CENTER_LEFT,
45  CENTER_RIGHT,
46  RIGHT,
47};
48}
49
50namespace views {
51
52Slider::Slider(SliderListener* listener, Orientation orientation)
53    : listener_(listener),
54      orientation_(orientation),
55      value_(0.f),
56      keyboard_increment_(0.1f),
57      animating_value_(0.f),
58      value_is_valid_(false),
59      accessibility_events_enabled_(true),
60      focus_border_color_(0),
61      bar_active_images_(kBarImagesActive),
62      bar_disabled_images_(kBarImagesDisabled) {
63  EnableCanvasFlippingForRTLUI(true);
64  set_focusable(true);
65  UpdateState(true);
66}
67
68Slider::~Slider() {
69}
70
71void Slider::SetValue(float value) {
72  SetValueInternal(value, VALUE_CHANGED_BY_API);
73}
74
75void Slider::SetKeyboardIncrement(float increment) {
76  keyboard_increment_ = increment;
77}
78
79void Slider::SetValueInternal(float value, SliderChangeReason reason) {
80  bool old_value_valid = value_is_valid_;
81
82  value_is_valid_ = true;
83  if (value < 0.0)
84   value = 0.0;
85  else if (value > 1.0)
86    value = 1.0;
87  if (value_ == value)
88    return;
89  float old_value = value_;
90  value_ = value;
91  if (listener_)
92    listener_->SliderValueChanged(this, value_, old_value, reason);
93
94  if (old_value_valid && MessageLoop::current()) {
95    // Do not animate when setting the value of the slider for the first time.
96    // There is no message-loop when running tests. So we cannot animate then.
97    animating_value_ = old_value;
98    move_animation_.reset(new ui::SlideAnimation(this));
99    move_animation_->SetSlideDuration(kSlideValueChangeDurationMS);
100    move_animation_->Show();
101    AnimationProgressed(move_animation_.get());
102  } else {
103    SchedulePaint();
104  }
105  if (accessibility_events_enabled_ && GetWidget()) {
106    GetWidget()->NotifyAccessibilityEvent(
107        this, ui::AccessibilityTypes::EVENT_VALUE_CHANGED, true);
108  }
109}
110
111void Slider::PrepareForMove(const gfx::Point& point) {
112  // Try to remember the position of the mouse cursor on the button.
113  gfx::Insets inset = GetInsets();
114  gfx::Rect content = GetContentsBounds();
115  float value = move_animation_.get() && move_animation_->is_animating() ?
116        animating_value_ : value_;
117
118  // For the horizontal orientation.
119  const int thumb_x = value * (content.width() - thumb_->width());
120  const int candidate_x = (base::i18n::IsRTL() ?
121      width() - (point.x() - inset.left()) :
122      point.x() - inset.left()) - thumb_x;
123  if (candidate_x >= 0 && candidate_x < thumb_->width())
124    initial_button_offset_.set_x(candidate_x);
125  else
126    initial_button_offset_.set_x(thumb_->width() / 2);
127
128  // For the vertical orientation.
129  const int thumb_y = (1.0 - value) * (content.height() - thumb_->height());
130  const int candidate_y = point.y() - thumb_y;
131  if (candidate_y >= 0 && candidate_y < thumb_->height())
132    initial_button_offset_.set_y(candidate_y);
133  else
134    initial_button_offset_.set_y(thumb_->height() / 2);
135}
136
137void Slider::MoveButtonTo(const gfx::Point& point) {
138  gfx::Insets inset = GetInsets();
139  // Calculate the value.
140  if (orientation_ == HORIZONTAL) {
141    int amount = base::i18n::IsRTL() ?
142        width() - inset.left() - point.x() - initial_button_offset_.x() :
143        point.x() - inset.left() - initial_button_offset_.x();
144    SetValueInternal(static_cast<float>(amount) /
145                         (width() - inset.width() - thumb_->width()),
146                     VALUE_CHANGED_BY_USER);
147  } else {
148    SetValueInternal(
149        1.0f - static_cast<float>(point.y() - initial_button_offset_.y()) /
150            (height() - thumb_->height()),
151        VALUE_CHANGED_BY_USER);
152  }
153}
154
155void Slider::UpdateState(bool control_on) {
156  ResourceBundle& rb = ResourceBundle::GetSharedInstance();
157  if (control_on) {
158    thumb_ = rb.GetImageNamed(IDR_SLIDER_ACTIVE_THUMB).ToImageSkia();
159    for (int i = 0; i < 4; ++i)
160      images_[i] = rb.GetImageNamed(bar_active_images_[i]).ToImageSkia();
161  } else {
162    thumb_ = rb.GetImageNamed(IDR_SLIDER_DISABLED_THUMB).ToImageSkia();
163    for (int i = 0; i < 4; ++i)
164      images_[i] = rb.GetImageNamed(bar_disabled_images_[i]).ToImageSkia();
165  }
166  bar_height_ = images_[LEFT]->height();
167  SchedulePaint();
168}
169
170void Slider::SetAccessibleName(const string16& name) {
171  accessible_name_ = name;
172}
173
174gfx::Size Slider::GetPreferredSize() {
175  const int kSizeMajor = 200;
176  const int kSizeMinor = 40;
177
178  if (orientation_ == HORIZONTAL)
179    return gfx::Size(std::max(width(), kSizeMajor), kSizeMinor);
180  return gfx::Size(kSizeMinor, std::max(height(), kSizeMajor));
181}
182
183void Slider::OnPaint(gfx::Canvas* canvas) {
184  gfx::Rect content = GetContentsBounds();
185  float value = move_animation_.get() && move_animation_->is_animating() ?
186      animating_value_ : value_;
187  if (orientation_ == HORIZONTAL) {
188    // Paint slider bar with image resources.
189
190    // Inset the slider bar a little bit, so that the left or the right end of
191    // the slider bar will not be exposed under the thumb button when the thumb
192    // button slides to the left most or right most position.
193    const int kBarInsetX = 2;
194    int bar_width = content.width() - kBarInsetX * 2;
195    int bar_cy = content.height() / 2 - bar_height_ / 2;
196
197    int w = content.width() - thumb_->width();
198    int full = value * w;
199    int middle = std::max(full, images_[LEFT]->width());
200
201    canvas->Save();
202    canvas->Translate(gfx::Vector2d(kBarInsetX, bar_cy));
203    canvas->DrawImageInt(*images_[LEFT], 0, 0);
204    canvas->DrawImageInt(*images_[RIGHT],
205                         bar_width - images_[RIGHT]->width(),
206                         0);
207    canvas->TileImageInt(*images_[CENTER_LEFT],
208                         images_[LEFT]->width(),
209                         0,
210                         middle - images_[LEFT]->width(),
211                         bar_height_);
212    canvas->TileImageInt(*images_[CENTER_RIGHT],
213                         middle,
214                         0,
215                         bar_width - middle - images_[RIGHT]->width(),
216                         bar_height_);
217    canvas->Restore();
218
219    // Paint slider thumb.
220    int button_cx = content.x() + full;
221    int thumb_y = content.height() / 2 - thumb_->height() / 2;
222    canvas->DrawImageInt(*thumb_, button_cx, thumb_y);
223  } else {
224    // TODO(jennyz): draw vertical slider bar with resources.
225    // TODO(sad): The painting code should use NativeTheme for various
226    // platforms.
227    const int kButtonRadius = thumb_->width() / 2;
228    const int kLineThickness = bar_height_ / 2;
229    const SkColor kFullColor = SkColorSetARGB(125, 0, 0, 0);
230    const SkColor kEmptyColor = SkColorSetARGB(50, 0, 0, 0);
231
232    int h = content.height() - thumb_->height();
233    int full = value * h;
234    int empty = h - full;
235    int x = content.width() / 2 - kLineThickness / 2;
236    canvas->FillRect(gfx::Rect(x, content.y() + kButtonRadius,
237                               kLineThickness, empty),
238                     kEmptyColor);
239    canvas->FillRect(gfx::Rect(x, content.y() + empty + 2 * kButtonRadius,
240                               kLineThickness, full),
241                     kFullColor);
242
243    // TODO(mtomasz): We draw a thumb here because so far it is the same
244    // for horizontal and vertical orientations. If it is different, then
245    // we will need a separate resource.
246    int button_cy = content.y() + h - full;
247    int thumb_x = content.width() / 2 - thumb_->width() / 2;
248    canvas->DrawImageInt(*thumb_, thumb_x, button_cy);
249  }
250  View::OnPaint(canvas);
251}
252
253bool Slider::OnMousePressed(const ui::MouseEvent& event) {
254  if (!event.IsOnlyLeftMouseButton())
255    return false;
256  if (listener_)
257    listener_->SliderDragStarted(this);
258  PrepareForMove(event.location());
259  MoveButtonTo(event.location());
260  return true;
261}
262
263bool Slider::OnMouseDragged(const ui::MouseEvent& event) {
264  MoveButtonTo(event.location());
265  return true;
266}
267
268void Slider::OnMouseReleased(const ui::MouseEvent& event) {
269  if (listener_)
270    listener_->SliderDragEnded(this);
271}
272
273bool Slider::OnKeyPressed(const ui::KeyEvent& event) {
274  if (orientation_ == HORIZONTAL) {
275    if (event.key_code() == ui::VKEY_LEFT) {
276      SetValueInternal(value_ - keyboard_increment_, VALUE_CHANGED_BY_USER);
277      return true;
278    } else if (event.key_code() == ui::VKEY_RIGHT) {
279      SetValueInternal(value_ + keyboard_increment_, VALUE_CHANGED_BY_USER);
280      return true;
281    }
282  } else {
283    if (event.key_code() == ui::VKEY_DOWN) {
284      SetValueInternal(value_ - keyboard_increment_, VALUE_CHANGED_BY_USER);
285      return true;
286    } else if (event.key_code() == ui::VKEY_UP) {
287      SetValueInternal(value_ + keyboard_increment_, VALUE_CHANGED_BY_USER);
288      return true;
289    }
290  }
291  return false;
292}
293
294ui::EventResult Slider::OnGestureEvent(ui::GestureEvent* event) {
295  if (event->type() == ui::ET_GESTURE_SCROLL_BEGIN ||
296      event->type() == ui::ET_GESTURE_TAP_DOWN) {
297    PrepareForMove(event->location());
298    MoveButtonTo(event->location());
299    return ui::ER_CONSUMED;
300  } else
301  if (event->type() == ui::ET_GESTURE_SCROLL_UPDATE ||
302      event->type() == ui::ET_GESTURE_SCROLL_END) {
303    MoveButtonTo(event->location());
304    return ui::ER_CONSUMED;
305  }
306  return ui::ER_UNHANDLED;
307}
308
309void Slider::AnimationProgressed(const ui::Animation* animation) {
310  animating_value_ = animation->CurrentValueBetween(animating_value_, value_);
311  SchedulePaint();
312}
313
314void Slider::GetAccessibleState(ui::AccessibleViewState* state) {
315  state->role = ui::AccessibilityTypes::ROLE_SLIDER;
316  state->name = accessible_name_;
317  state->value = UTF8ToUTF16(
318      base::StringPrintf("%d%%", (int)(value_ * 100 + 0.5)));
319}
320
321void Slider::OnPaintFocusBorder(gfx::Canvas* canvas) {
322  if (!focus_border_color_) {
323    View::OnPaintFocusBorder(canvas);
324  } else if (HasFocus() && (focusable() || IsAccessibilityFocusable())) {
325    canvas->DrawRect(gfx::Rect(1, 1, width() - 3, height() - 3),
326                     focus_border_color_);
327  }
328}
329
330}  // namespace views
331