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