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/message_loop.h"
9 #include "base/strings/stringprintf.h"
10 #include "base/strings/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/resource/resource_bundle.h"
17 #include "ui/events/event.h"
18 #include "ui/gfx/animation/slide_animation.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
24 namespace {
25 const int kSlideValueChangeDurationMS = 150;
26
27 const int kBarImagesActive[] = {
28 IDR_SLIDER_ACTIVE_LEFT,
29 IDR_SLIDER_ACTIVE_CENTER,
30 IDR_SLIDER_PRESSED_CENTER,
31 IDR_SLIDER_PRESSED_RIGHT,
32 };
33
34 const 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.
42 enum BorderElements {
43 LEFT,
44 CENTER_LEFT,
45 CENTER_RIGHT,
46 RIGHT,
47 };
48 }
49
50 namespace views {
51
Slider(SliderListener * listener,Orientation orientation)52 Slider::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 SetFocusable(true);
65 UpdateState(true);
66 }
67
~Slider()68 Slider::~Slider() {
69 }
70
SetValue(float value)71 void Slider::SetValue(float value) {
72 SetValueInternal(value, VALUE_CHANGED_BY_API);
73 }
74
SetKeyboardIncrement(float increment)75 void Slider::SetKeyboardIncrement(float increment) {
76 keyboard_increment_ = increment;
77 }
78
SetValueInternal(float value,SliderChangeReason reason)79 void 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 && base::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 gfx::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 NotifyAccessibilityEvent(
107 ui::AccessibilityTypes::EVENT_VALUE_CHANGED, true);
108 }
109 }
110
PrepareForMove(const gfx::Point & point)111 void 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
MoveButtonTo(const gfx::Point & point)137 void 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
UpdateState(bool control_on)155 void 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
SetAccessibleName(const string16 & name)170 void Slider::SetAccessibleName(const string16& name) {
171 accessible_name_ = name;
172 }
173
OnPaintFocus(gfx::Canvas * canvas)174 void Slider::OnPaintFocus(gfx::Canvas* canvas) {
175 if (!HasFocus())
176 return;
177
178 if (!focus_border_color_) {
179 canvas->DrawFocusRect(GetLocalBounds());
180 } else if (HasFocus()) {
181 canvas->DrawSolidFocusRect(
182 gfx::Rect(1, 1, width() - 3, height() - 3),
183 focus_border_color_);
184 }
185 }
186
GetPreferredSize()187 gfx::Size Slider::GetPreferredSize() {
188 const int kSizeMajor = 200;
189 const int kSizeMinor = 40;
190
191 if (orientation_ == HORIZONTAL)
192 return gfx::Size(std::max(width(), kSizeMajor), kSizeMinor);
193 return gfx::Size(kSizeMinor, std::max(height(), kSizeMajor));
194 }
195
OnPaint(gfx::Canvas * canvas)196 void Slider::OnPaint(gfx::Canvas* canvas) {
197 gfx::Rect content = GetContentsBounds();
198 float value = move_animation_.get() && move_animation_->is_animating() ?
199 animating_value_ : value_;
200 if (orientation_ == HORIZONTAL) {
201 // Paint slider bar with image resources.
202
203 // Inset the slider bar a little bit, so that the left or the right end of
204 // the slider bar will not be exposed under the thumb button when the thumb
205 // button slides to the left most or right most position.
206 const int kBarInsetX = 2;
207 int bar_width = content.width() - kBarInsetX * 2;
208 int bar_cy = content.height() / 2 - bar_height_ / 2;
209
210 int w = content.width() - thumb_->width();
211 int full = value * w;
212 int middle = std::max(full, images_[LEFT]->width());
213
214 canvas->Save();
215 canvas->Translate(gfx::Vector2d(kBarInsetX, bar_cy));
216 canvas->DrawImageInt(*images_[LEFT], 0, 0);
217 canvas->DrawImageInt(*images_[RIGHT],
218 bar_width - images_[RIGHT]->width(),
219 0);
220 canvas->TileImageInt(*images_[CENTER_LEFT],
221 images_[LEFT]->width(),
222 0,
223 middle - images_[LEFT]->width(),
224 bar_height_);
225 canvas->TileImageInt(*images_[CENTER_RIGHT],
226 middle,
227 0,
228 bar_width - middle - images_[RIGHT]->width(),
229 bar_height_);
230 canvas->Restore();
231
232 // Paint slider thumb.
233 int button_cx = content.x() + full;
234 int thumb_y = content.height() / 2 - thumb_->height() / 2;
235 canvas->DrawImageInt(*thumb_, button_cx, thumb_y);
236 } else {
237 // TODO(jennyz): draw vertical slider bar with resources.
238 // TODO(sad): The painting code should use NativeTheme for various
239 // platforms.
240 const int kButtonRadius = thumb_->width() / 2;
241 const int kLineThickness = bar_height_ / 2;
242 const SkColor kFullColor = SkColorSetARGB(125, 0, 0, 0);
243 const SkColor kEmptyColor = SkColorSetARGB(50, 0, 0, 0);
244
245 int h = content.height() - thumb_->height();
246 int full = value * h;
247 int empty = h - full;
248 int x = content.width() / 2 - kLineThickness / 2;
249 canvas->FillRect(gfx::Rect(x, content.y() + kButtonRadius,
250 kLineThickness, empty),
251 kEmptyColor);
252 canvas->FillRect(gfx::Rect(x, content.y() + empty + 2 * kButtonRadius,
253 kLineThickness, full),
254 kFullColor);
255
256 // TODO(mtomasz): We draw a thumb here because so far it is the same
257 // for horizontal and vertical orientations. If it is different, then
258 // we will need a separate resource.
259 int button_cy = content.y() + h - full;
260 int thumb_x = content.width() / 2 - thumb_->width() / 2;
261 canvas->DrawImageInt(*thumb_, thumb_x, button_cy);
262 }
263 View::OnPaint(canvas);
264 OnPaintFocus(canvas);
265 }
266
OnMousePressed(const ui::MouseEvent & event)267 bool Slider::OnMousePressed(const ui::MouseEvent& event) {
268 if (!event.IsOnlyLeftMouseButton())
269 return false;
270 if (listener_)
271 listener_->SliderDragStarted(this);
272 PrepareForMove(event.location());
273 MoveButtonTo(event.location());
274 return true;
275 }
276
OnMouseDragged(const ui::MouseEvent & event)277 bool Slider::OnMouseDragged(const ui::MouseEvent& event) {
278 MoveButtonTo(event.location());
279 return true;
280 }
281
OnMouseReleased(const ui::MouseEvent & event)282 void Slider::OnMouseReleased(const ui::MouseEvent& event) {
283 if (listener_)
284 listener_->SliderDragEnded(this);
285 }
286
OnKeyPressed(const ui::KeyEvent & event)287 bool Slider::OnKeyPressed(const ui::KeyEvent& event) {
288 if (orientation_ == HORIZONTAL) {
289 if (event.key_code() == ui::VKEY_LEFT) {
290 SetValueInternal(value_ - keyboard_increment_, VALUE_CHANGED_BY_USER);
291 return true;
292 } else if (event.key_code() == ui::VKEY_RIGHT) {
293 SetValueInternal(value_ + keyboard_increment_, VALUE_CHANGED_BY_USER);
294 return true;
295 }
296 } else {
297 if (event.key_code() == ui::VKEY_DOWN) {
298 SetValueInternal(value_ - keyboard_increment_, VALUE_CHANGED_BY_USER);
299 return true;
300 } else if (event.key_code() == ui::VKEY_UP) {
301 SetValueInternal(value_ + keyboard_increment_, VALUE_CHANGED_BY_USER);
302 return true;
303 }
304 }
305 return false;
306 }
307
OnGestureEvent(ui::GestureEvent * event)308 void Slider::OnGestureEvent(ui::GestureEvent* event) {
309 if (event->type() == ui::ET_GESTURE_SCROLL_BEGIN ||
310 event->type() == ui::ET_GESTURE_TAP_DOWN) {
311 PrepareForMove(event->location());
312 MoveButtonTo(event->location());
313 event->SetHandled();
314 } else if (event->type() == ui::ET_GESTURE_SCROLL_UPDATE ||
315 event->type() == ui::ET_GESTURE_SCROLL_END) {
316 MoveButtonTo(event->location());
317 event->SetHandled();
318 }
319 }
320
AnimationProgressed(const gfx::Animation * animation)321 void Slider::AnimationProgressed(const gfx::Animation* animation) {
322 animating_value_ = animation->CurrentValueBetween(animating_value_, value_);
323 SchedulePaint();
324 }
325
GetAccessibleState(ui::AccessibleViewState * state)326 void Slider::GetAccessibleState(ui::AccessibleViewState* state) {
327 state->role = ui::AccessibilityTypes::ROLE_SLIDER;
328 state->name = accessible_name_;
329 state->value = UTF8ToUTF16(
330 base::StringPrintf("%d%%", (int)(value_ * 100 + 0.5)));
331 }
332
333 } // namespace views
334