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/label.h" 6 7#include <algorithm> 8#include <cmath> 9#include <limits> 10#include <vector> 11 12#include "base/i18n/rtl.h" 13#include "base/logging.h" 14#include "base/strings/string_split.h" 15#include "base/strings/string_util.h" 16#include "base/strings/utf_string_conversions.h" 17#include "ui/accessibility/ax_view_state.h" 18#include "ui/gfx/canvas.h" 19#include "ui/gfx/color_utils.h" 20#include "ui/gfx/insets.h" 21#include "ui/gfx/text_elider.h" 22#include "ui/gfx/text_utils.h" 23#include "ui/gfx/utf16_indexing.h" 24#include "ui/native_theme/native_theme.h" 25#include "ui/views/background.h" 26 27namespace { 28 29const int kCachedSizeLimit = 10; 30const base::char16 kPasswordReplacementChar = '*'; 31 32} // namespace 33 34namespace views { 35 36// static 37const char Label::kViewClassName[] = "Label"; 38const int Label::kFocusBorderPadding = 1; 39 40Label::Label() { 41 Init(base::string16(), gfx::FontList()); 42} 43 44Label::Label(const base::string16& text) { 45 Init(text, gfx::FontList()); 46} 47 48Label::Label(const base::string16& text, const gfx::FontList& font_list) { 49 Init(text, font_list); 50} 51 52Label::~Label() { 53} 54 55void Label::SetFontList(const gfx::FontList& font_list) { 56 font_list_ = font_list; 57 ResetCachedSize(); 58 PreferredSizeChanged(); 59 SchedulePaint(); 60} 61 62void Label::SetText(const base::string16& text) { 63 if (text != text_) 64 SetTextInternal(text); 65} 66 67void Label::SetTextInternal(const base::string16& text) { 68 text_ = text; 69 70 if (obscured_) { 71 size_t obscured_text_length = 72 static_cast<size_t>(gfx::UTF16IndexToOffset(text_, 0, text_.length())); 73 layout_text_.assign(obscured_text_length, kPasswordReplacementChar); 74 } else { 75 layout_text_ = text_; 76 } 77 78 ResetCachedSize(); 79 PreferredSizeChanged(); 80 SchedulePaint(); 81} 82 83void Label::SetAutoColorReadabilityEnabled(bool enabled) { 84 auto_color_readability_ = enabled; 85 RecalculateColors(); 86} 87 88void Label::SetEnabledColor(SkColor color) { 89 requested_enabled_color_ = color; 90 enabled_color_set_ = true; 91 RecalculateColors(); 92} 93 94void Label::SetDisabledColor(SkColor color) { 95 requested_disabled_color_ = color; 96 disabled_color_set_ = true; 97 RecalculateColors(); 98} 99 100void Label::SetBackgroundColor(SkColor color) { 101 background_color_ = color; 102 background_color_set_ = true; 103 RecalculateColors(); 104} 105 106void Label::SetShadows(const gfx::ShadowValues& shadows) { 107 shadows_ = shadows; 108 text_size_valid_ = false; 109} 110 111void Label::SetSubpixelRenderingEnabled(bool subpixel_rendering_enabled) { 112 subpixel_rendering_enabled_ = subpixel_rendering_enabled; 113} 114 115void Label::SetHorizontalAlignment(gfx::HorizontalAlignment alignment) { 116 // If the UI layout is right-to-left, flip the alignment direction. 117 if (base::i18n::IsRTL() && 118 (alignment == gfx::ALIGN_LEFT || alignment == gfx::ALIGN_RIGHT)) { 119 alignment = (alignment == gfx::ALIGN_LEFT) ? 120 gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT; 121 } 122 if (horizontal_alignment_ != alignment) { 123 horizontal_alignment_ = alignment; 124 SchedulePaint(); 125 } 126} 127 128gfx::HorizontalAlignment Label::GetHorizontalAlignment() const { 129 if (horizontal_alignment_ != gfx::ALIGN_TO_HEAD) 130 return horizontal_alignment_; 131 132 const base::i18n::TextDirection dir = 133 base::i18n::GetFirstStrongCharacterDirection(layout_text_); 134 return dir == base::i18n::RIGHT_TO_LEFT ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT; 135} 136 137void Label::SetLineHeight(int height) { 138 if (height != line_height_) { 139 line_height_ = height; 140 ResetCachedSize(); 141 PreferredSizeChanged(); 142 SchedulePaint(); 143 } 144} 145 146void Label::SetMultiLine(bool multi_line) { 147 DCHECK(!multi_line || (elide_behavior_ == gfx::ELIDE_TAIL || 148 elide_behavior_ == gfx::NO_ELIDE)); 149 if (multi_line != multi_line_) { 150 multi_line_ = multi_line; 151 ResetCachedSize(); 152 PreferredSizeChanged(); 153 SchedulePaint(); 154 } 155} 156 157void Label::SetObscured(bool obscured) { 158 if (obscured != obscured_) { 159 obscured_ = obscured; 160 SetTextInternal(text_); 161 } 162} 163 164void Label::SetAllowCharacterBreak(bool allow_character_break) { 165 if (allow_character_break != allow_character_break_) { 166 allow_character_break_ = allow_character_break; 167 ResetCachedSize(); 168 PreferredSizeChanged(); 169 SchedulePaint(); 170 } 171} 172 173void Label::SetElideBehavior(gfx::ElideBehavior elide_behavior) { 174 DCHECK(!multi_line_ || (elide_behavior_ == gfx::ELIDE_TAIL || 175 elide_behavior_ == gfx::NO_ELIDE)); 176 if (elide_behavior != elide_behavior_) { 177 elide_behavior_ = elide_behavior; 178 ResetCachedSize(); 179 PreferredSizeChanged(); 180 SchedulePaint(); 181 } 182} 183 184void Label::SetTooltipText(const base::string16& tooltip_text) { 185 tooltip_text_ = tooltip_text; 186} 187 188void Label::SizeToFit(int max_width) { 189 DCHECK(multi_line_); 190 191 std::vector<base::string16> lines; 192 base::SplitString(layout_text_, '\n', &lines); 193 194 int label_width = 0; 195 for (std::vector<base::string16>::const_iterator iter = lines.begin(); 196 iter != lines.end(); ++iter) { 197 label_width = std::max(label_width, gfx::GetStringWidth(*iter, font_list_)); 198 } 199 200 label_width += GetInsets().width(); 201 202 if (max_width > 0) 203 label_width = std::min(label_width, max_width); 204 205 SetBounds(x(), y(), label_width, 0); 206 SizeToPreferredSize(); 207} 208 209const base::string16& Label::GetLayoutTextForTesting() const { 210 return layout_text_; 211} 212 213gfx::Insets Label::GetInsets() const { 214 gfx::Insets insets = View::GetInsets(); 215 if (focusable()) { 216 insets += gfx::Insets(kFocusBorderPadding, kFocusBorderPadding, 217 kFocusBorderPadding, kFocusBorderPadding); 218 } 219 return insets; 220} 221 222int Label::GetBaseline() const { 223 return GetInsets().top() + font_list_.GetBaseline(); 224} 225 226gfx::Size Label::GetPreferredSize() const { 227 // Return a size of (0, 0) if the label is not visible and if the 228 // collapse_when_hidden_ flag is set. 229 // TODO(munjal): This logic probably belongs to the View class. But for now, 230 // put it here since putting it in View class means all inheriting classes 231 // need ot respect the collapse_when_hidden_ flag. 232 if (!visible() && collapse_when_hidden_) 233 return gfx::Size(); 234 235 gfx::Size size(GetTextSize()); 236 gfx::Insets insets = GetInsets(); 237 size.Enlarge(insets.width(), insets.height()); 238 return size; 239} 240 241gfx::Size Label::GetMinimumSize() const { 242 gfx::Size text_size(GetTextSize()); 243 if ((!visible() && collapse_when_hidden_) || text_size.IsEmpty()) 244 return gfx::Size(); 245 246 gfx::Size size(gfx::GetStringWidth(base::string16(gfx::kEllipsisUTF16), 247 font_list_), 248 font_list_.GetHeight()); 249 size.SetToMin(text_size); // The actual text may be shorter than an ellipsis. 250 gfx::Insets insets = GetInsets(); 251 size.Enlarge(insets.width(), insets.height()); 252 return size; 253} 254 255int Label::GetHeightForWidth(int w) const { 256 if (!multi_line_) 257 return View::GetHeightForWidth(w); 258 259 w = std::max(0, w - GetInsets().width()); 260 261 for (size_t i = 0; i < cached_heights_.size(); ++i) { 262 const gfx::Size& s = cached_heights_[i]; 263 if (s.width() == w) 264 return s.height() + GetInsets().height(); 265 } 266 267 int cache_width = w; 268 269 int h = font_list_.GetHeight(); 270 const int flags = ComputeDrawStringFlags(); 271 gfx::Canvas::SizeStringInt( 272 layout_text_, font_list_, &w, &h, line_height_, flags); 273 cached_heights_[cached_heights_cursor_] = gfx::Size(cache_width, h); 274 cached_heights_cursor_ = (cached_heights_cursor_ + 1) % kCachedSizeLimit; 275 return h + GetInsets().height(); 276} 277 278const char* Label::GetClassName() const { 279 return kViewClassName; 280} 281 282View* Label::GetTooltipHandlerForPoint(const gfx::Point& point) { 283 if (tooltip_text_.empty() && !ShouldShowDefaultTooltip()) 284 return NULL; 285 286 return HitTestPoint(point) ? this : NULL; 287} 288 289bool Label::CanProcessEventsWithinSubtree() const { 290 // Send events to the parent view for handling. 291 return false; 292} 293 294void Label::GetAccessibleState(ui::AXViewState* state) { 295 state->role = ui::AX_ROLE_STATIC_TEXT; 296 state->AddStateFlag(ui::AX_STATE_READ_ONLY); 297 state->name = layout_text_; 298} 299 300bool Label::GetTooltipText(const gfx::Point& p, base::string16* tooltip) const { 301 if (!tooltip_text_.empty()) { 302 tooltip->assign(tooltip_text_); 303 return true; 304 } 305 306 if (ShouldShowDefaultTooltip()) { 307 *tooltip = layout_text_; 308 return true; 309 } 310 311 return false; 312} 313 314void Label::PaintText(gfx::Canvas* canvas, 315 const base::string16& text, 316 const gfx::Rect& text_bounds, 317 int flags) { 318 SkColor color = enabled() ? actual_enabled_color_ : actual_disabled_color_; 319 if (elide_behavior_ == gfx::FADE_TAIL) { 320 canvas->DrawFadedString(text, font_list_, color, text_bounds, flags); 321 } else { 322 canvas->DrawStringRectWithShadows(text, font_list_, color, text_bounds, 323 line_height_, flags, shadows_); 324 } 325 326 if (HasFocus()) { 327 gfx::Rect focus_bounds = text_bounds; 328 focus_bounds.Inset(-kFocusBorderPadding, -kFocusBorderPadding); 329 canvas->DrawFocusRect(focus_bounds); 330 } 331} 332 333gfx::Size Label::GetTextSize() const { 334 if (!text_size_valid_) { 335 // For single-line strings, we supply the largest possible width, because 336 // while adding NO_ELLIPSIS to the flags works on Windows for forcing 337 // SizeStringInt() to calculate the desired width, it doesn't seem to work 338 // on Linux. 339 int w = multi_line_ ? 340 GetAvailableRect().width() : std::numeric_limits<int>::max(); 341 int h = font_list_.GetHeight(); 342 // For single-line strings, ignore the available width and calculate how 343 // wide the text wants to be. 344 int flags = ComputeDrawStringFlags(); 345 if (!multi_line_) 346 flags |= gfx::Canvas::NO_ELLIPSIS; 347 gfx::Canvas::SizeStringInt( 348 layout_text_, font_list_, &w, &h, line_height_, flags); 349 text_size_.SetSize(w, h); 350 const gfx::Insets shadow_margin = -gfx::ShadowValue::GetMargin(shadows_); 351 text_size_.Enlarge(shadow_margin.width(), shadow_margin.height()); 352 text_size_valid_ = true; 353 } 354 355 return text_size_; 356} 357 358void Label::OnBoundsChanged(const gfx::Rect& previous_bounds) { 359 text_size_valid_ &= !multi_line_; 360} 361 362void Label::OnPaint(gfx::Canvas* canvas) { 363 OnPaintBackground(canvas); 364 // We skip painting the focus border because it is being handled seperately by 365 // some subclasses of Label. We do not want View's focus border painting to 366 // interfere with that. 367 OnPaintBorder(canvas); 368 369 base::string16 paint_text; 370 gfx::Rect text_bounds; 371 int flags = 0; 372 CalculateDrawStringParams(&paint_text, &text_bounds, &flags); 373 PaintText(canvas, paint_text, text_bounds, flags); 374} 375 376void Label::OnNativeThemeChanged(const ui::NativeTheme* theme) { 377 UpdateColorsFromTheme(theme); 378} 379 380void Label::Init(const base::string16& text, const gfx::FontList& font_list) { 381 font_list_ = font_list; 382 enabled_color_set_ = disabled_color_set_ = background_color_set_ = false; 383 subpixel_rendering_enabled_ = true; 384 auto_color_readability_ = true; 385 UpdateColorsFromTheme(ui::NativeTheme::instance()); 386 horizontal_alignment_ = gfx::ALIGN_CENTER; 387 line_height_ = 0; 388 multi_line_ = false; 389 obscured_ = false; 390 allow_character_break_ = false; 391 elide_behavior_ = gfx::ELIDE_TAIL; 392 collapse_when_hidden_ = false; 393 cached_heights_.resize(kCachedSizeLimit); 394 ResetCachedSize(); 395 396 SetText(text); 397} 398 399void Label::RecalculateColors() { 400 actual_enabled_color_ = auto_color_readability_ ? 401 color_utils::GetReadableColor(requested_enabled_color_, 402 background_color_) : 403 requested_enabled_color_; 404 actual_disabled_color_ = auto_color_readability_ ? 405 color_utils::GetReadableColor(requested_disabled_color_, 406 background_color_) : 407 requested_disabled_color_; 408} 409 410gfx::Rect Label::GetTextBounds() const { 411 gfx::Rect available(GetAvailableRect()); 412 gfx::Size text_size(GetTextSize()); 413 text_size.set_width(std::min(available.width(), text_size.width())); 414 gfx::Point origin(GetInsets().left(), GetInsets().top()); 415 switch (GetHorizontalAlignment()) { 416 case gfx::ALIGN_LEFT: 417 break; 418 case gfx::ALIGN_CENTER: 419 // Put any extra margin pixel on the left to match the legacy behavior 420 // from the use of GetTextExtentPoint32() on Windows. 421 origin.Offset((available.width() + 1 - text_size.width()) / 2, 0); 422 break; 423 case gfx::ALIGN_RIGHT: 424 origin.set_x(available.right() - text_size.width()); 425 break; 426 default: 427 NOTREACHED(); 428 break; 429 } 430 if (!multi_line_) 431 text_size.set_height(available.height()); 432 // Support vertical centering of multi-line labels: http://crbug.com/429595 433 origin.Offset(0, std::max(0, (available.height() - text_size.height())) / 2); 434 return gfx::Rect(origin, text_size); 435} 436 437int Label::ComputeDrawStringFlags() const { 438 int flags = 0; 439 440 // We can't use subpixel rendering if the background is non-opaque. 441 if (SkColorGetA(background_color_) != 0xFF || !subpixel_rendering_enabled_) 442 flags |= gfx::Canvas::NO_SUBPIXEL_RENDERING; 443 444 base::i18n::TextDirection direction = 445 base::i18n::GetFirstStrongCharacterDirection(layout_text_); 446 if (direction == base::i18n::RIGHT_TO_LEFT) 447 flags |= gfx::Canvas::FORCE_RTL_DIRECTIONALITY; 448 else 449 flags |= gfx::Canvas::FORCE_LTR_DIRECTIONALITY; 450 451 switch (GetHorizontalAlignment()) { 452 case gfx::ALIGN_LEFT: 453 flags |= gfx::Canvas::TEXT_ALIGN_LEFT; 454 break; 455 case gfx::ALIGN_CENTER: 456 flags |= gfx::Canvas::TEXT_ALIGN_CENTER; 457 break; 458 case gfx::ALIGN_RIGHT: 459 flags |= gfx::Canvas::TEXT_ALIGN_RIGHT; 460 break; 461 default: 462 NOTREACHED(); 463 break; 464 } 465 466 if (!multi_line_) 467 return flags; 468 469 flags |= gfx::Canvas::MULTI_LINE; 470#if !defined(OS_WIN) 471 // Don't elide multiline labels on Linux. 472 // Todo(davemoore): Do we depend on eliding multiline text? 473 // Pango insists on limiting the number of lines to one if text is 474 // elided. You can get around this if you can pass a maximum height 475 // but we don't currently have that data when we call the pango code. 476 flags |= gfx::Canvas::NO_ELLIPSIS; 477#endif 478 if (allow_character_break_) 479 flags |= gfx::Canvas::CHARACTER_BREAK; 480 481 return flags; 482} 483 484gfx::Rect Label::GetAvailableRect() const { 485 gfx::Rect bounds(size()); 486 bounds.Inset(GetInsets()); 487 return bounds; 488} 489 490void Label::CalculateDrawStringParams(base::string16* paint_text, 491 gfx::Rect* text_bounds, 492 int* flags) const { 493 DCHECK(paint_text && text_bounds && flags); 494 495 const bool forbid_ellipsis = elide_behavior_ == gfx::NO_ELIDE || 496 elide_behavior_ == gfx::FADE_TAIL; 497 if (multi_line_ || forbid_ellipsis) { 498 *paint_text = layout_text_; 499 } else { 500 *paint_text = gfx::ElideText(layout_text_, font_list_, 501 GetAvailableRect().width(), elide_behavior_); 502 } 503 504 *text_bounds = GetTextBounds(); 505 *flags = ComputeDrawStringFlags(); 506 // TODO(msw): Elide multi-line text with ElideRectangleText instead. 507 if (!multi_line_ || forbid_ellipsis) 508 *flags |= gfx::Canvas::NO_ELLIPSIS; 509} 510 511void Label::UpdateColorsFromTheme(const ui::NativeTheme* theme) { 512 if (!enabled_color_set_) { 513 requested_enabled_color_ = theme->GetSystemColor( 514 ui::NativeTheme::kColorId_LabelEnabledColor); 515 } 516 if (!disabled_color_set_) { 517 requested_disabled_color_ = theme->GetSystemColor( 518 ui::NativeTheme::kColorId_LabelDisabledColor); 519 } 520 if (!background_color_set_) { 521 background_color_ = theme->GetSystemColor( 522 ui::NativeTheme::kColorId_LabelBackgroundColor); 523 } 524 RecalculateColors(); 525} 526 527void Label::ResetCachedSize() { 528 text_size_valid_ = false; 529 cached_heights_cursor_ = 0; 530 for (int i = 0; i < kCachedSizeLimit; ++i) 531 cached_heights_[i] = gfx::Size(); 532} 533 534bool Label::ShouldShowDefaultTooltip() const { 535 const gfx::Size text_size = GetTextSize(); 536 const gfx::Size size = GetContentsBounds().size(); 537 return !obscured() && (text_size.width() > size.width() || 538 (multi_line_ && text_size.height() > size.height())); 539} 540 541} // namespace views 542