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/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 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 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 set_focusable(true); 65 UpdateState(true); 66 } 67 68 Slider::~Slider() { 69 } 70 71 void Slider::SetValue(float value) { 72 SetValueInternal(value, VALUE_CHANGED_BY_API); 73 } 74 75 void Slider::SetKeyboardIncrement(float increment) { 76 keyboard_increment_ = increment; 77 } 78 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 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 NotifyAccessibilityEvent( 107 ui::AccessibilityTypes::EVENT_VALUE_CHANGED, true); 108 } 109 } 110 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 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 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 170 void Slider::SetAccessibleName(const string16& name) { 171 accessible_name_ = name; 172 } 173 174 gfx::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 183 void 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 253 bool 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 263 bool Slider::OnMouseDragged(const ui::MouseEvent& event) { 264 MoveButtonTo(event.location()); 265 return true; 266 } 267 268 void Slider::OnMouseReleased(const ui::MouseEvent& event) { 269 if (listener_) 270 listener_->SliderDragEnded(this); 271 } 272 273 bool 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 294 void 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 event->SetHandled(); 300 } else if (event->type() == ui::ET_GESTURE_SCROLL_UPDATE || 301 event->type() == ui::ET_GESTURE_SCROLL_END) { 302 MoveButtonTo(event->location()); 303 event->SetHandled(); 304 } 305 } 306 307 void Slider::AnimationProgressed(const ui::Animation* animation) { 308 animating_value_ = animation->CurrentValueBetween(animating_value_, value_); 309 SchedulePaint(); 310 } 311 312 void Slider::GetAccessibleState(ui::AccessibleViewState* state) { 313 state->role = ui::AccessibilityTypes::ROLE_SLIDER; 314 state->name = accessible_name_; 315 state->value = UTF8ToUTF16( 316 base::StringPrintf("%d%%", (int)(value_ * 100 + 0.5))); 317 } 318 319 void Slider::OnPaintFocusBorder(gfx::Canvas* canvas) { 320 if (!focus_border_color_) { 321 View::OnPaintFocusBorder(canvas); 322 } else if (HasFocus() && (focusable() || IsAccessibilityFocusable())) { 323 canvas->DrawRect(gfx::Rect(1, 1, width() - 3, height() - 3), 324 focus_border_color_); 325 } 326 } 327 328 } // namespace views 329