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