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/lazy_instance.h" 8 #include "base/logging.h" 9 #include "ui/gfx/animation/throb_animation.h" 10 #include "ui/gfx/canvas.h" 11 #include "ui/gfx/font_list.h" 12 #include "ui/gfx/sys_color_change_listener.h" 13 #include "ui/native_theme/native_theme.h" 14 #include "ui/views/background.h" 15 #include "ui/views/controls/button/label_button_border.h" 16 #include "ui/views/painter.h" 17 #include "ui/views/window/dialog_delegate.h" 18 19 #if defined(OS_LINUX) && !defined(OS_CHROMEOS) 20 #include "ui/views/linux_ui/linux_ui.h" 21 #endif 22 23 namespace { 24 25 // The default spacing between the icon and text. 26 const int kSpacing = 5; 27 28 #if !(defined(OS_LINUX) && !defined(OS_CHROMEOS)) 29 // Default text and shadow colors for STYLE_BUTTON. 30 const SkColor kStyleButtonTextColor = SK_ColorBLACK; 31 const SkColor kStyleButtonShadowColor = SK_ColorWHITE; 32 #endif 33 34 const gfx::FontList& GetDefaultNormalFontList() { 35 static base::LazyInstance<gfx::FontList>::Leaky font_list = 36 LAZY_INSTANCE_INITIALIZER; 37 return font_list.Get(); 38 } 39 40 const gfx::FontList& GetDefaultBoldFontList() { 41 static base::LazyInstance<gfx::FontList>::Leaky font_list = 42 LAZY_INSTANCE_INITIALIZER; 43 if ((font_list.Get().GetFontStyle() & gfx::Font::BOLD) == 0) { 44 font_list.Get() = font_list.Get(). 45 DeriveWithStyle(font_list.Get().GetFontStyle() | gfx::Font::BOLD); 46 DCHECK_NE(font_list.Get().GetFontStyle() & gfx::Font::BOLD, 0); 47 } 48 return font_list.Get(); 49 } 50 51 } // namespace 52 53 namespace views { 54 55 // static 56 const int LabelButton::kHoverAnimationDurationMs = 170; 57 58 // static 59 const char LabelButton::kViewClassName[] = "LabelButton"; 60 61 LabelButton::LabelButton(ButtonListener* listener, const base::string16& text) 62 : CustomButton(listener), 63 image_(new ImageView()), 64 label_(new Label()), 65 cached_normal_font_list_(GetDefaultNormalFontList()), 66 cached_bold_font_list_(GetDefaultBoldFontList()), 67 button_state_images_(), 68 button_state_colors_(), 69 explicitly_set_colors_(), 70 is_default_(false), 71 style_(STYLE_TEXTBUTTON), 72 border_is_themed_border_(true), 73 image_label_spacing_(kSpacing) { 74 SetAnimationDuration(kHoverAnimationDurationMs); 75 SetText(text); 76 77 AddChildView(image_); 78 image_->set_interactive(false); 79 80 AddChildView(label_); 81 label_->SetFontList(cached_normal_font_list_); 82 label_->SetAutoColorReadabilityEnabled(false); 83 label_->SetHorizontalAlignment(gfx::ALIGN_LEFT); 84 85 // Initialize the colors, border, and layout. 86 SetStyle(style_); 87 88 SetAccessibleName(text); 89 } 90 91 LabelButton::~LabelButton() {} 92 93 const gfx::ImageSkia& LabelButton::GetImage(ButtonState for_state) { 94 if (for_state != STATE_NORMAL && button_state_images_[for_state].isNull()) 95 return button_state_images_[STATE_NORMAL]; 96 return button_state_images_[for_state]; 97 } 98 99 void LabelButton::SetImage(ButtonState for_state, const gfx::ImageSkia& image) { 100 button_state_images_[for_state] = image; 101 UpdateImage(); 102 } 103 104 const base::string16& LabelButton::GetText() const { 105 return label_->text(); 106 } 107 108 void LabelButton::SetText(const base::string16& text) { 109 SetAccessibleName(text); 110 label_->SetText(text); 111 } 112 113 void LabelButton::SetTextColor(ButtonState for_state, SkColor color) { 114 button_state_colors_[for_state] = color; 115 if (for_state == STATE_DISABLED) 116 label_->SetDisabledColor(color); 117 else if (for_state == state()) 118 label_->SetEnabledColor(color); 119 explicitly_set_colors_[for_state] = true; 120 } 121 122 void LabelButton::SetTextShadows(const gfx::ShadowValues& shadows) { 123 label_->SetShadows(shadows); 124 } 125 126 void LabelButton::SetTextSubpixelRenderingEnabled(bool enabled) { 127 label_->SetSubpixelRenderingEnabled(enabled); 128 } 129 130 bool LabelButton::GetTextMultiLine() const { 131 return label_->multi_line(); 132 } 133 134 void LabelButton::SetTextMultiLine(bool text_multi_line) { 135 label_->SetMultiLine(text_multi_line); 136 } 137 138 const gfx::FontList& LabelButton::GetFontList() const { 139 return label_->font_list(); 140 } 141 142 void LabelButton::SetFontList(const gfx::FontList& font_list) { 143 cached_normal_font_list_ = font_list; 144 cached_bold_font_list_ = font_list.DeriveWithStyle( 145 font_list.GetFontStyle() | gfx::Font::BOLD); 146 147 // STYLE_BUTTON uses bold text to indicate default buttons. 148 label_->SetFontList( 149 style_ == STYLE_BUTTON && is_default_ ? 150 cached_bold_font_list_ : cached_normal_font_list_); 151 } 152 153 void LabelButton::SetElideBehavior(gfx::ElideBehavior elide_behavior) { 154 label_->SetElideBehavior(elide_behavior); 155 } 156 157 gfx::HorizontalAlignment LabelButton::GetHorizontalAlignment() const { 158 return label_->GetHorizontalAlignment(); 159 } 160 161 void LabelButton::SetHorizontalAlignment(gfx::HorizontalAlignment alignment) { 162 label_->SetHorizontalAlignment(alignment); 163 InvalidateLayout(); 164 } 165 166 void LabelButton::SetMinSize(const gfx::Size& min_size) { 167 min_size_ = min_size; 168 ResetCachedPreferredSize(); 169 } 170 171 void LabelButton::SetMaxSize(const gfx::Size& max_size) { 172 max_size_ = max_size; 173 ResetCachedPreferredSize(); 174 } 175 176 void LabelButton::SetIsDefault(bool is_default) { 177 if (is_default == is_default_) 178 return; 179 is_default_ = is_default; 180 ui::Accelerator accel(ui::VKEY_RETURN, ui::EF_NONE); 181 is_default_ ? AddAccelerator(accel) : RemoveAccelerator(accel); 182 183 // STYLE_BUTTON uses bold text to indicate default buttons. 184 if (style_ == STYLE_BUTTON) { 185 label_->SetFontList( 186 is_default ? cached_bold_font_list_ : cached_normal_font_list_); 187 } 188 } 189 190 void LabelButton::SetStyle(ButtonStyle style) { 191 style_ = style; 192 // Inset the button focus rect from the actual border; roughly match Windows. 193 if (style == STYLE_BUTTON) { 194 SetFocusPainter(scoped_ptr<Painter>()); 195 } else { 196 SetFocusPainter(Painter::CreateDashedFocusPainterWithInsets( 197 gfx::Insets(3, 3, 3, 3))); 198 } 199 if (style == STYLE_BUTTON) { 200 label_->SetHorizontalAlignment(gfx::ALIGN_CENTER); 201 SetFocusable(true); 202 } 203 if (style == STYLE_BUTTON) 204 SetMinSize(gfx::Size(70, 33)); 205 OnNativeThemeChanged(GetNativeTheme()); 206 ResetCachedPreferredSize(); 207 } 208 209 void LabelButton::SetImageLabelSpacing(int spacing) { 210 if (spacing == image_label_spacing_) 211 return; 212 image_label_spacing_ = spacing; 213 ResetCachedPreferredSize(); 214 InvalidateLayout(); 215 } 216 217 void LabelButton::SetFocusPainter(scoped_ptr<Painter> focus_painter) { 218 focus_painter_ = focus_painter.Pass(); 219 } 220 221 gfx::Size LabelButton::GetPreferredSize() const { 222 if (cached_preferred_size_valid_) 223 return cached_preferred_size_; 224 225 // Use a temporary label copy for sizing to avoid calculation side-effects. 226 Label label(GetText(), cached_normal_font_list_); 227 label.SetShadows(label_->shadows()); 228 label.SetMultiLine(GetTextMultiLine()); 229 230 if (style() == STYLE_BUTTON) { 231 // Some text appears wider when rendered normally than when rendered bold. 232 // Accommodate the widest, as buttons may show bold and shouldn't resize. 233 const int current_width = label.GetPreferredSize().width(); 234 label.SetFontList(cached_bold_font_list_); 235 if (label.GetPreferredSize().width() < current_width) 236 label.SetFontList(cached_normal_font_list_); 237 } 238 239 // Calculate the required size. 240 const gfx::Size image_size(image_->GetPreferredSize()); 241 gfx::Size size(label.GetPreferredSize()); 242 if (image_size.width() > 0 && size.width() > 0) 243 size.Enlarge(image_label_spacing_, 0); 244 size.SetToMax(gfx::Size(0, image_size.height())); 245 const gfx::Insets insets(GetInsets()); 246 size.Enlarge(image_size.width() + insets.width(), insets.height()); 247 248 // Make the size at least as large as the minimum size needed by the border. 249 size.SetToMax(border() ? border()->GetMinimumSize() : gfx::Size()); 250 251 // Increase the minimum size monotonically with the preferred size. 252 size.SetToMax(min_size_); 253 min_size_ = size; 254 255 // Return the largest known size clamped to the maximum size (if valid). 256 if (max_size_.width() > 0) 257 size.set_width(std::min(max_size_.width(), size.width())); 258 if (max_size_.height() > 0) 259 size.set_height(std::min(max_size_.height(), size.height())); 260 261 // Cache this computed size, as recomputing it is an expensive operation. 262 cached_preferred_size_valid_ = true; 263 cached_preferred_size_ = size; 264 return cached_preferred_size_; 265 } 266 267 int LabelButton::GetHeightForWidth(int w) const { 268 w -= GetInsets().width(); 269 const gfx::Size image_size(image_->GetPreferredSize()); 270 w -= image_size.width(); 271 if (image_size.width() > 0 && !GetText().empty()) 272 w -= image_label_spacing_; 273 274 int height = std::max(image_size.height(), label_->GetHeightForWidth(w)); 275 if (border()) 276 height = std::max(height, border()->GetMinimumSize().height()); 277 278 height = std::max(height, min_size_.height()); 279 if (max_size_.height() > 0) 280 height = std::min(height, max_size_.height()); 281 return height; 282 } 283 284 void LabelButton::Layout() { 285 gfx::HorizontalAlignment adjusted_alignment = GetHorizontalAlignment(); 286 if (base::i18n::IsRTL() && adjusted_alignment != gfx::ALIGN_CENTER) 287 adjusted_alignment = (adjusted_alignment == gfx::ALIGN_LEFT) ? 288 gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT; 289 290 gfx::Rect child_area(GetChildAreaBounds()); 291 child_area.Inset(GetInsets()); 292 293 gfx::Size image_size(image_->GetPreferredSize()); 294 image_size.SetToMin(child_area.size()); 295 296 // The label takes any remaining width after sizing the image, unless both 297 // views are centered. In that case, using the tighter preferred label width 298 // avoids wasted space within the label that would look like awkward padding. 299 // Labels can paint over the full button height, including the border height. 300 gfx::Size label_size(child_area.width(), height()); 301 if (!image_size.IsEmpty() && !label_size.IsEmpty()) { 302 label_size.set_width(std::max(child_area.width() - 303 image_size.width() - image_label_spacing_, 0)); 304 if (adjusted_alignment == gfx::ALIGN_CENTER) { 305 // Ensure multi-line labels paired with images use their available width. 306 label_size.set_width( 307 std::min(label_size.width(), label_->GetPreferredSize().width())); 308 } 309 } 310 311 gfx::Point image_origin(child_area.origin()); 312 image_origin.Offset(0, (child_area.height() - image_size.height()) / 2); 313 if (adjusted_alignment == gfx::ALIGN_CENTER) { 314 const int spacing = (image_size.width() > 0 && label_size.width() > 0) ? 315 image_label_spacing_ : 0; 316 const int total_width = image_size.width() + label_size.width() + 317 spacing; 318 image_origin.Offset((child_area.width() - total_width) / 2, 0); 319 } else if (adjusted_alignment == gfx::ALIGN_RIGHT) { 320 image_origin.Offset(child_area.width() - image_size.width(), 0); 321 } 322 323 gfx::Point label_origin(child_area.x(), 0); 324 if (!image_size.IsEmpty() && adjusted_alignment != gfx::ALIGN_RIGHT) { 325 label_origin.set_x(image_origin.x() + image_size.width() + 326 image_label_spacing_); 327 } 328 329 image_->SetBoundsRect(gfx::Rect(image_origin, image_size)); 330 label_->SetBoundsRect(gfx::Rect(label_origin, label_size)); 331 } 332 333 const char* LabelButton::GetClassName() const { 334 return kViewClassName; 335 } 336 337 scoped_ptr<LabelButtonBorder> LabelButton::CreateDefaultBorder() const { 338 return scoped_ptr<LabelButtonBorder>(new LabelButtonBorder(style_)); 339 } 340 341 void LabelButton::SetBorder(scoped_ptr<Border> border) { 342 border_is_themed_border_ = false; 343 View::SetBorder(border.Pass()); 344 ResetCachedPreferredSize(); 345 } 346 347 gfx::Rect LabelButton::GetChildAreaBounds() { 348 return GetLocalBounds(); 349 } 350 351 void LabelButton::OnPaint(gfx::Canvas* canvas) { 352 View::OnPaint(canvas); 353 Painter::PaintFocusPainter(this, canvas, focus_painter_.get()); 354 } 355 356 void LabelButton::OnFocus() { 357 View::OnFocus(); 358 // Typically the border renders differently when focused. 359 SchedulePaint(); 360 } 361 362 void LabelButton::OnBlur() { 363 View::OnBlur(); 364 // Typically the border renders differently when focused. 365 SchedulePaint(); 366 } 367 368 void LabelButton::GetExtraParams(ui::NativeTheme::ExtraParams* params) const { 369 params->button.checked = false; 370 params->button.indeterminate = false; 371 params->button.is_default = is_default_; 372 params->button.is_focused = HasFocus() && IsAccessibilityFocusable(); 373 params->button.has_border = false; 374 params->button.classic_state = 0; 375 params->button.background_color = label_->background_color(); 376 } 377 378 void LabelButton::ResetColorsFromNativeTheme() { 379 const ui::NativeTheme* theme = GetNativeTheme(); 380 SkColor colors[STATE_COUNT] = { 381 theme->GetSystemColor(ui::NativeTheme::kColorId_ButtonEnabledColor), 382 theme->GetSystemColor(ui::NativeTheme::kColorId_ButtonHoverColor), 383 theme->GetSystemColor(ui::NativeTheme::kColorId_ButtonHoverColor), 384 theme->GetSystemColor(ui::NativeTheme::kColorId_ButtonDisabledColor), 385 }; 386 387 // Certain styles do not change text color when hovered or pressed. 388 bool constant_text_color = false; 389 // Use hardcoded colors for inverted color scheme support and STYLE_BUTTON. 390 if (gfx::IsInvertedColorScheme()) { 391 constant_text_color = true; 392 colors[STATE_NORMAL] = SK_ColorWHITE; 393 label_->SetBackgroundColor(SK_ColorBLACK); 394 label_->set_background(Background::CreateSolidBackground(SK_ColorBLACK)); 395 label_->SetAutoColorReadabilityEnabled(true); 396 label_->SetShadows(gfx::ShadowValues()); 397 } else if (style() == STYLE_BUTTON) { 398 // TODO(erg): This is disabled on desktop linux because of the binary asset 399 // confusion. These details should either be pushed into ui::NativeThemeWin 400 // or should be obsoleted by rendering buttons with paint calls instead of 401 // with static assets. http://crbug.com/350498 402 #if !(defined(OS_LINUX) && !defined(OS_CHROMEOS)) 403 constant_text_color = true; 404 colors[STATE_NORMAL] = kStyleButtonTextColor; 405 label_->SetBackgroundColor(theme->GetSystemColor( 406 ui::NativeTheme::kColorId_ButtonBackgroundColor)); 407 label_->SetAutoColorReadabilityEnabled(false); 408 label_->SetShadows(gfx::ShadowValues( 409 1, gfx::ShadowValue(gfx::Point(0, 1), 0, kStyleButtonShadowColor))); 410 #endif 411 label_->set_background(NULL); 412 } else { 413 label_->set_background(NULL); 414 } 415 416 if (constant_text_color) 417 colors[STATE_HOVERED] = colors[STATE_PRESSED] = colors[STATE_NORMAL]; 418 419 for (size_t state = STATE_NORMAL; state < STATE_COUNT; ++state) { 420 if (!explicitly_set_colors_[state]) { 421 SetTextColor(static_cast<ButtonState>(state), colors[state]); 422 explicitly_set_colors_[state] = false; 423 } 424 } 425 } 426 427 void LabelButton::UpdateImage() { 428 image_->SetImage(GetImage(state())); 429 ResetCachedPreferredSize(); 430 } 431 432 void LabelButton::UpdateThemedBorder() { 433 // Don't override borders set by others. 434 if (!border_is_themed_border_) 435 return; 436 437 scoped_ptr<LabelButtonBorder> label_button_border = CreateDefaultBorder(); 438 439 #if defined(OS_LINUX) && !defined(OS_CHROMEOS) 440 views::LinuxUI* linux_ui = views::LinuxUI::instance(); 441 if (linux_ui) { 442 SetBorder(linux_ui->CreateNativeBorder( 443 this, label_button_border.Pass())); 444 } else 445 #endif 446 { 447 SetBorder(label_button_border.PassAs<Border>()); 448 } 449 450 border_is_themed_border_ = true; 451 } 452 453 void LabelButton::StateChanged() { 454 const gfx::Size previous_image_size(image_->GetPreferredSize()); 455 UpdateImage(); 456 const SkColor color = button_state_colors_[state()]; 457 if (state() != STATE_DISABLED && label_->enabled_color() != color) 458 label_->SetEnabledColor(color); 459 label_->SetEnabled(state() != STATE_DISABLED); 460 if (image_->GetPreferredSize() != previous_image_size) 461 Layout(); 462 } 463 464 void LabelButton::ChildPreferredSizeChanged(View* child) { 465 ResetCachedPreferredSize(); 466 PreferredSizeChanged(); 467 } 468 469 void LabelButton::OnNativeThemeChanged(const ui::NativeTheme* theme) { 470 ResetColorsFromNativeTheme(); 471 UpdateThemedBorder(); 472 // Invalidate the layout to pickup the new insets from the border. 473 InvalidateLayout(); 474 } 475 476 ui::NativeTheme::Part LabelButton::GetThemePart() const { 477 return ui::NativeTheme::kPushButton; 478 } 479 480 gfx::Rect LabelButton::GetThemePaintRect() const { 481 return GetLocalBounds(); 482 } 483 484 ui::NativeTheme::State LabelButton::GetThemeState( 485 ui::NativeTheme::ExtraParams* params) const { 486 GetExtraParams(params); 487 switch (state()) { 488 case STATE_NORMAL: return ui::NativeTheme::kNormal; 489 case STATE_HOVERED: return ui::NativeTheme::kHovered; 490 case STATE_PRESSED: return ui::NativeTheme::kPressed; 491 case STATE_DISABLED: return ui::NativeTheme::kDisabled; 492 case STATE_COUNT: NOTREACHED() << "Unknown state: " << state(); 493 } 494 return ui::NativeTheme::kNormal; 495 } 496 497 const gfx::Animation* LabelButton::GetThemeAnimation() const { 498 return hover_animation_.get(); 499 } 500 501 ui::NativeTheme::State LabelButton::GetBackgroundThemeState( 502 ui::NativeTheme::ExtraParams* params) const { 503 GetExtraParams(params); 504 return ui::NativeTheme::kNormal; 505 } 506 507 ui::NativeTheme::State LabelButton::GetForegroundThemeState( 508 ui::NativeTheme::ExtraParams* params) const { 509 GetExtraParams(params); 510 return ui::NativeTheme::kHovered; 511 } 512 513 void LabelButton::ResetCachedPreferredSize() { 514 cached_preferred_size_valid_ = false; 515 cached_preferred_size_= gfx::Size(); 516 } 517 518 } // namespace views 519