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/button/label_button.h"
6
7#include "base/logging.h"
8#include "grit/ui_resources.h"
9#include "ui/base/resource/resource_bundle.h"
10#include "ui/gfx/animation/throb_animation.h"
11#include "ui/gfx/sys_color_change_listener.h"
12#include "ui/native_theme/native_theme.h"
13#include "ui/views/controls/button/label_button_border.h"
14#include "ui/views/painter.h"
15#include "ui/views/window/dialog_delegate.h"
16
17#if defined(OS_WIN)
18#include "ui/native_theme/native_theme_win.h"
19#endif
20
21namespace {
22
23// The spacing between the icon and text.
24const int kSpacing = 5;
25
26// Default text and shadow colors for STYLE_BUTTON.
27const SkColor kStyleButtonTextColor = SK_ColorBLACK;
28const SkColor kStyleButtonShadowColor = SK_ColorWHITE;
29
30}  // namespace
31
32namespace views {
33
34// static
35const int LabelButton::kHoverAnimationDurationMs = 170;
36
37// static
38const char LabelButton::kViewClassName[] = "LabelButton";
39
40LabelButton::LabelButton(ButtonListener* listener, const string16& text)
41    : CustomButton(listener),
42      image_(new ImageView()),
43      label_(new Label()),
44      button_state_images_(),
45      button_state_colors_(),
46      explicitly_set_colors_(),
47      is_default_(false),
48      style_(STYLE_TEXTBUTTON) {
49  SetAnimationDuration(kHoverAnimationDurationMs);
50  SetText(text);
51
52  AddChildView(image_);
53  image_->set_interactive(false);
54
55  AddChildView(label_);
56  label_->SetAutoColorReadabilityEnabled(false);
57  label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
58
59  // Initialize the colors, border, and layout.
60  SetStyle(style_);
61
62  SetAccessibleName(text);
63}
64
65LabelButton::~LabelButton() {}
66
67const gfx::ImageSkia& LabelButton::GetImage(ButtonState for_state) {
68  if (for_state != STATE_NORMAL && button_state_images_[for_state].isNull())
69    return button_state_images_[STATE_NORMAL];
70  return button_state_images_[for_state];
71}
72
73void LabelButton::SetImage(ButtonState for_state, const gfx::ImageSkia& image) {
74  button_state_images_[for_state] = image;
75  UpdateImage();
76}
77
78const string16& LabelButton::GetText() const {
79  return label_->text();
80}
81
82void LabelButton::SetText(const string16& text) {
83  SetAccessibleName(text);
84  label_->SetText(text);
85}
86
87void LabelButton::SetTextColor(ButtonState for_state, SkColor color) {
88  button_state_colors_[for_state] = color;
89  if (for_state == STATE_DISABLED)
90    label_->SetDisabledColor(color);
91  else if (for_state == state())
92    label_->SetEnabledColor(color);
93  explicitly_set_colors_[for_state] = true;
94}
95
96bool LabelButton::GetTextMultiLine() const {
97  return label_->is_multi_line();
98}
99
100void LabelButton::SetTextMultiLine(bool text_multi_line) {
101  label_->SetMultiLine(text_multi_line);
102}
103
104const gfx::Font& LabelButton::GetFont() const {
105  return label_->font();
106}
107
108void LabelButton::SetFont(const gfx::Font& font) {
109  label_->SetFont(font);
110}
111
112void LabelButton::SetElideBehavior(Label::ElideBehavior elide_behavior) {
113  label_->SetElideBehavior(elide_behavior);
114}
115
116gfx::HorizontalAlignment LabelButton::GetHorizontalAlignment() const {
117  return label_->horizontal_alignment();
118}
119
120void LabelButton::SetHorizontalAlignment(gfx::HorizontalAlignment alignment) {
121  label_->SetHorizontalAlignment(alignment);
122  InvalidateLayout();
123}
124
125void LabelButton::SetIsDefault(bool is_default) {
126  if (is_default == is_default_)
127    return;
128  is_default_ = is_default;
129  ui::Accelerator accel(ui::VKEY_RETURN, ui::EF_NONE);
130  is_default_ ? AddAccelerator(accel) : RemoveAccelerator(accel);
131
132  // STYLE_BUTTON uses bold text to indicate default buttons.
133  if (style_ == STYLE_BUTTON) {
134    int style = label_->font().GetStyle();
135    style = is_default ? style | gfx::Font::BOLD : style & ~gfx::Font::BOLD;
136    label_->SetFont(label_->font().DeriveFont(0, style));
137  }
138}
139
140void LabelButton::SetStyle(ButtonStyle style) {
141  // Use the new button style instead of the native button style.
142  // TODO(msw): Officialy deprecate and remove STYLE_NATIVE_TEXTBUTTON.
143  if (style == STYLE_NATIVE_TEXTBUTTON)
144    style = STYLE_BUTTON;
145
146  style_ = style;
147  set_border(new LabelButtonBorder(style));
148  // Inset the button focus rect from the actual border; roughly match Windows.
149  if (style == STYLE_BUTTON) {
150    SetFocusPainter(scoped_ptr<Painter>());
151  } else {
152    SetFocusPainter(Painter::CreateDashedFocusPainterWithInsets(
153                        gfx::Insets(3, 3, 3, 3)));
154  }
155  if (style == STYLE_BUTTON || style == STYLE_NATIVE_TEXTBUTTON) {
156    label_->SetHorizontalAlignment(gfx::ALIGN_CENTER);
157    SetFocusable(true);
158  }
159  if (style == STYLE_BUTTON)
160    set_min_size(gfx::Size(70, 33));
161  // Invalidate the layout to pickup the new insets from the border.
162  InvalidateLayout();
163  ResetColorsFromNativeTheme();
164}
165
166void LabelButton::SetFocusPainter(scoped_ptr<Painter> focus_painter) {
167  focus_painter_ = focus_painter.Pass();
168}
169
170gfx::Size LabelButton::GetPreferredSize() {
171  // Use a temporary label copy for sizing to avoid calculation side-effects.
172  gfx::Font font = GetFont();
173  Label label(GetText(), font);
174  label.SetMultiLine(GetTextMultiLine());
175
176  if (style() == STYLE_BUTTON) {
177    // Some text appears wider when rendered normally than when rendered bold.
178    // Accommodate the widest, as buttons may show bold and shouldn't resize.
179    const int current_width = label.GetPreferredSize().width();
180    label.SetFont(font.DeriveFont(0, font.GetStyle() ^ gfx::Font::BOLD));
181    if (label.GetPreferredSize().width() < current_width)
182      label.SetFont(font);
183  }
184
185  // Resize multi-line labels given the current limited available width.
186  const gfx::Size image_size(image_->GetPreferredSize());
187  const int image_width = image_size.width();
188  if (GetTextMultiLine() && (width() > image_width + kSpacing))
189    label.SizeToFit(width() - image_width - (image_width > 0 ? kSpacing : 0));
190
191  // Calculate the required size.
192  gfx::Size size(label.GetPreferredSize());
193  if (image_width > 0 && size.width() > 0)
194    size.Enlarge(kSpacing, 0);
195  size.SetToMax(gfx::Size(0, image_size.height()));
196  const gfx::Insets insets(GetInsets());
197  size.Enlarge(image_size.width() + insets.width(), insets.height());
198
199  // Make the size at least as large as the minimum size needed by the border.
200  size.SetToMax(border()->GetMinimumSize());
201
202  // Increase the minimum size monotonically with the preferred size.
203  size.SetToMax(min_size_);
204  min_size_ = size;
205
206  // Return the largest known size clamped to the maximum size (if valid).
207  if (max_size_.width() > 0)
208    size.set_width(std::min(max_size_.width(), size.width()));
209  if (max_size_.height() > 0)
210    size.set_height(std::min(max_size_.height(), size.height()));
211  return size;
212}
213
214void LabelButton::Layout() {
215  gfx::HorizontalAlignment adjusted_alignment = GetHorizontalAlignment();
216  if (base::i18n::IsRTL() && adjusted_alignment != gfx::ALIGN_CENTER)
217    adjusted_alignment = (adjusted_alignment == gfx::ALIGN_LEFT) ?
218        gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT;
219
220  gfx::Rect child_area(GetLocalBounds());
221  child_area.Inset(GetInsets());
222
223  gfx::Size image_size(image_->GetPreferredSize());
224  image_size.set_width(std::min(image_size.width(), child_area.width()));
225  image_size.set_height(std::min(image_size.height(), child_area.height()));
226
227  // The label takes any remaining width after sizing the image, unless both
228  // views are centered. In that case, using the tighter preferred label width
229  // avoids wasted space within the label that would look like awkward padding.
230  gfx::Size label_size(child_area.size());
231  if (!image_size.IsEmpty() && !label_size.IsEmpty()) {
232    label_size.set_width(
233        std::max(child_area.width() - image_size.width() - kSpacing, 0));
234    if (adjusted_alignment == gfx::ALIGN_CENTER) {
235      // Ensure multi-line labels paired with images use their available width.
236      if (GetTextMultiLine())
237        label_->SizeToFit(label_size.width());
238      label_size.set_width(
239          std::min(label_size.width(), label_->GetPreferredSize().width()));
240    }
241  }
242
243  gfx::Point image_origin(child_area.origin());
244  image_origin.Offset(0, (child_area.height() - image_size.height()) / 2);
245  if (adjusted_alignment == gfx::ALIGN_CENTER) {
246    const int total_width = image_size.width() + label_size.width() +
247        ((image_size.width() > 0 && label_size.width() > 0) ? kSpacing : 0);
248    image_origin.Offset((child_area.width() - total_width) / 2, 0);
249  } else if (adjusted_alignment == gfx::ALIGN_RIGHT) {
250    image_origin.Offset(child_area.width() - image_size.width(), 0);
251  }
252
253  gfx::Point label_origin(child_area.origin());
254  if (!image_size.IsEmpty() &&adjusted_alignment != gfx::ALIGN_RIGHT)
255    label_origin.set_x(image_origin.x() + image_size.width() + kSpacing);
256
257  image_->SetBoundsRect(gfx::Rect(image_origin, image_size));
258  label_->SetBoundsRect(gfx::Rect(label_origin, label_size));
259}
260
261const char* LabelButton::GetClassName() const {
262  return kViewClassName;
263}
264
265void LabelButton::OnPaint(gfx::Canvas* canvas) {
266  View::OnPaint(canvas);
267  Painter::PaintFocusPainter(this, canvas, focus_painter_.get());
268}
269
270void LabelButton::OnFocus() {
271  View::OnFocus();
272  // Typically the border renders differently when focused.
273  SchedulePaint();
274}
275
276void LabelButton::OnBlur() {
277  View::OnBlur();
278  // Typically the border renders differently when focused.
279  SchedulePaint();
280}
281
282void LabelButton::GetExtraParams(ui::NativeTheme::ExtraParams* params) const {
283  params->button.checked = false;
284  params->button.indeterminate = false;
285  params->button.is_default = is_default_;
286  params->button.is_focused = HasFocus() && IsAccessibilityFocusable();
287  params->button.has_border = style() == STYLE_NATIVE_TEXTBUTTON;
288  params->button.classic_state = 0;
289  params->button.background_color = label()->background_color();
290}
291
292void LabelButton::ResetColorsFromNativeTheme() {
293  const ui::NativeTheme* theme = GetNativeTheme();
294  SkColor colors[STATE_COUNT] = {
295    theme->GetSystemColor(ui::NativeTheme::kColorId_ButtonEnabledColor),
296    theme->GetSystemColor(ui::NativeTheme::kColorId_ButtonHoverColor),
297    theme->GetSystemColor(ui::NativeTheme::kColorId_ButtonHoverColor),
298    theme->GetSystemColor(ui::NativeTheme::kColorId_ButtonDisabledColor),
299  };
300
301  // Certain styles do not change text color when hovered or pressed.
302  bool constant_text_color = false;
303#if defined(OS_WIN)
304  constant_text_color |= (style() == STYLE_NATIVE_TEXTBUTTON &&
305                          theme == ui::NativeThemeWin::instance());
306#endif
307
308  // Use hardcoded colors for inverted color scheme support and STYLE_BUTTON.
309  if (gfx::IsInvertedColorScheme()) {
310    constant_text_color = true;
311    colors[STATE_NORMAL] = SK_ColorWHITE;
312    label_->SetBackgroundColor(SK_ColorBLACK);
313    label_->SetAutoColorReadabilityEnabled(true);
314    label_->ClearEmbellishing();
315  } else if (style() == STYLE_BUTTON) {
316    constant_text_color = true;
317    colors[STATE_NORMAL] = kStyleButtonTextColor;
318    label_->SetBackgroundColor(theme->GetSystemColor(
319        ui::NativeTheme::kColorId_ButtonBackgroundColor));
320    label_->SetAutoColorReadabilityEnabled(false);
321    label_->SetShadowColors(kStyleButtonShadowColor, kStyleButtonShadowColor);
322    label_->SetShadowOffset(0, 1);
323  }
324
325  if (constant_text_color)
326    colors[STATE_HOVERED] = colors[STATE_PRESSED] = colors[STATE_NORMAL];
327
328  for (size_t state = STATE_NORMAL; state < STATE_COUNT; ++state) {
329    if (!explicitly_set_colors_[state]) {
330      SetTextColor(static_cast<ButtonState>(state), colors[state]);
331      explicitly_set_colors_[state] = false;
332    }
333  }
334}
335
336void LabelButton::UpdateImage() {
337  image_->SetImage(GetImage(state()));
338}
339
340void LabelButton::StateChanged() {
341  const gfx::Size previous_image_size(image_->GetPreferredSize());
342  UpdateImage();
343  const SkColor color = button_state_colors_[state()];
344  if (state() != STATE_DISABLED && label_->enabled_color() != color)
345    label_->SetEnabledColor(color);
346  label_->SetEnabled(state() != STATE_DISABLED);
347  if (image_->GetPreferredSize() != previous_image_size)
348    Layout();
349}
350
351void LabelButton::ChildPreferredSizeChanged(View* child) {
352  PreferredSizeChanged();
353}
354
355void LabelButton::OnNativeThemeChanged(const ui::NativeTheme* theme) {
356  ResetColorsFromNativeTheme();
357}
358
359ui::NativeTheme::Part LabelButton::GetThemePart() const {
360  return ui::NativeTheme::kPushButton;
361}
362
363gfx::Rect LabelButton::GetThemePaintRect() const {
364  return GetLocalBounds();
365}
366
367ui::NativeTheme::State LabelButton::GetThemeState(
368    ui::NativeTheme::ExtraParams* params) const {
369  GetExtraParams(params);
370  switch (state()) {
371    case STATE_NORMAL:   return ui::NativeTheme::kNormal;
372    case STATE_HOVERED:  return ui::NativeTheme::kHovered;
373    case STATE_PRESSED:  return ui::NativeTheme::kPressed;
374    case STATE_DISABLED: return ui::NativeTheme::kDisabled;
375    case STATE_COUNT:    NOTREACHED() << "Unknown state: " << state();
376  }
377  return ui::NativeTheme::kNormal;
378}
379
380const gfx::Animation* LabelButton::GetThemeAnimation() const {
381#if defined(OS_WIN)
382  if (style() == STYLE_NATIVE_TEXTBUTTON &&
383      GetNativeTheme() == ui::NativeThemeWin::instance()) {
384    return ui::NativeThemeWin::instance()->IsThemingActive() ?
385        hover_animation_.get() : NULL;
386  }
387#endif
388  return hover_animation_.get();
389}
390
391ui::NativeTheme::State LabelButton::GetBackgroundThemeState(
392    ui::NativeTheme::ExtraParams* params) const {
393  GetExtraParams(params);
394  return ui::NativeTheme::kNormal;
395}
396
397ui::NativeTheme::State LabelButton::GetForegroundThemeState(
398    ui::NativeTheme::ExtraParams* params) const {
399  GetExtraParams(params);
400  return ui::NativeTheme::kHovered;
401}
402
403}  // namespace views
404