| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/views/controls/label.h" |
| |
| #include <algorithm> |
| #include <cmath> |
| #include <limits> |
| #include <vector> |
| |
| #include "base/i18n/rtl.h" |
| #include "base/logging.h" |
| #include "base/profiler/scoped_tracker.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "ui/accessibility/ax_view_state.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/color_utils.h" |
| #include "ui/gfx/geometry/insets.h" |
| #include "ui/gfx/text_elider.h" |
| #include "ui/native_theme/native_theme.h" |
| |
| namespace views { |
| |
| // static |
| const char Label::kViewClassName[] = "Label"; |
| const int Label::kFocusBorderPadding = 1; |
| |
| Label::Label() { |
| Init(base::string16(), gfx::FontList()); |
| } |
| |
| Label::Label(const base::string16& text) { |
| Init(text, gfx::FontList()); |
| } |
| |
| Label::Label(const base::string16& text, const gfx::FontList& font_list) { |
| Init(text, font_list); |
| } |
| |
| Label::~Label() { |
| } |
| |
| void Label::SetFontList(const gfx::FontList& font_list) { |
| is_first_paint_text_ = true; |
| render_text_->SetFontList(font_list); |
| ResetLayout(); |
| } |
| |
| void Label::SetText(const base::string16& new_text) { |
| if (new_text == text()) |
| return; |
| is_first_paint_text_ = true; |
| render_text_->SetText(new_text); |
| ResetLayout(); |
| } |
| |
| void Label::SetAutoColorReadabilityEnabled(bool enabled) { |
| if (auto_color_readability_ == enabled) |
| return; |
| is_first_paint_text_ = true; |
| auto_color_readability_ = enabled; |
| RecalculateColors(); |
| } |
| |
| void Label::SetEnabledColor(SkColor color) { |
| if (enabled_color_set_ && requested_enabled_color_ == color) |
| return; |
| is_first_paint_text_ = true; |
| requested_enabled_color_ = color; |
| enabled_color_set_ = true; |
| RecalculateColors(); |
| } |
| |
| void Label::SetDisabledColor(SkColor color) { |
| if (disabled_color_set_ && requested_disabled_color_ == color) |
| return; |
| is_first_paint_text_ = true; |
| requested_disabled_color_ = color; |
| disabled_color_set_ = true; |
| RecalculateColors(); |
| } |
| |
| void Label::SetBackgroundColor(SkColor color) { |
| if (background_color_set_ && background_color_ == color) |
| return; |
| is_first_paint_text_ = true; |
| background_color_ = color; |
| background_color_set_ = true; |
| RecalculateColors(); |
| } |
| |
| void Label::SetShadows(const gfx::ShadowValues& shadows) { |
| // TODO(mukai): early exit if the specified shadows are same. |
| is_first_paint_text_ = true; |
| render_text_->set_shadows(shadows); |
| ResetLayout(); |
| } |
| |
| void Label::SetSubpixelRenderingEnabled(bool subpixel_rendering_enabled) { |
| if (subpixel_rendering_enabled_ == subpixel_rendering_enabled) |
| return; |
| is_first_paint_text_ = true; |
| subpixel_rendering_enabled_ = subpixel_rendering_enabled; |
| RecalculateColors(); |
| } |
| |
| void Label::SetHorizontalAlignment(gfx::HorizontalAlignment alignment) { |
| // If the UI layout is right-to-left, flip the alignment direction. |
| if (base::i18n::IsRTL() && |
| (alignment == gfx::ALIGN_LEFT || alignment == gfx::ALIGN_RIGHT)) { |
| alignment = (alignment == gfx::ALIGN_LEFT) ? |
| gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT; |
| } |
| if (horizontal_alignment() == alignment) |
| return; |
| is_first_paint_text_ = true; |
| render_text_->SetHorizontalAlignment(alignment); |
| ResetLayout(); |
| } |
| |
| void Label::SetLineHeight(int height) { |
| if (line_height() == height) |
| return; |
| is_first_paint_text_ = true; |
| render_text_->SetMinLineHeight(height); |
| ResetLayout(); |
| } |
| |
| void Label::SetMultiLine(bool multi_line) { |
| DCHECK(!multi_line || (elide_behavior_ == gfx::ELIDE_TAIL || |
| elide_behavior_ == gfx::NO_ELIDE)); |
| if (this->multi_line() == multi_line) |
| return; |
| is_first_paint_text_ = true; |
| multi_line_ = multi_line; |
| if (render_text_->MultilineSupported()) |
| render_text_->SetMultiline(multi_line); |
| render_text_->SetReplaceNewlineCharsWithSymbols(!multi_line); |
| ResetLayout(); |
| } |
| |
| void Label::SetObscured(bool obscured) { |
| if (this->obscured() == obscured) |
| return; |
| is_first_paint_text_ = true; |
| render_text_->SetObscured(obscured); |
| ResetLayout(); |
| } |
| |
| void Label::SetAllowCharacterBreak(bool allow_character_break) { |
| const gfx::WordWrapBehavior behavior = |
| allow_character_break ? gfx::WRAP_LONG_WORDS : gfx::TRUNCATE_LONG_WORDS; |
| if (render_text_->word_wrap_behavior() == behavior) |
| return; |
| render_text_->SetWordWrapBehavior(behavior); |
| if (multi_line()) { |
| is_first_paint_text_ = true; |
| ResetLayout(); |
| } |
| } |
| |
| void Label::SetElideBehavior(gfx::ElideBehavior elide_behavior) { |
| DCHECK(!multi_line() || (elide_behavior_ == gfx::ELIDE_TAIL || |
| elide_behavior_ == gfx::NO_ELIDE)); |
| if (elide_behavior_ == elide_behavior) |
| return; |
| is_first_paint_text_ = true; |
| elide_behavior_ = elide_behavior; |
| ResetLayout(); |
| } |
| |
| void Label::SetTooltipText(const base::string16& tooltip_text) { |
| DCHECK(handles_tooltips_); |
| tooltip_text_ = tooltip_text; |
| } |
| |
| void Label::SetHandlesTooltips(bool enabled) { |
| handles_tooltips_ = enabled; |
| } |
| |
| void Label::SizeToFit(int max_width) { |
| DCHECK(multi_line()); |
| max_width_ = max_width; |
| SizeToPreferredSize(); |
| } |
| |
| base::string16 Label::GetDisplayTextForTesting() { |
| lines_.clear(); |
| MaybeBuildRenderTextLines(); |
| base::string16 result; |
| if (lines_.empty()) |
| return result; |
| result.append(lines_[0]->GetDisplayText()); |
| for (size_t i = 1; i < lines_.size(); ++i) { |
| result.append(1, '\n'); |
| result.append(lines_[i]->GetDisplayText()); |
| } |
| return result; |
| } |
| |
| gfx::Insets Label::GetInsets() const { |
| gfx::Insets insets = View::GetInsets(); |
| if (focusable()) { |
| insets += gfx::Insets(kFocusBorderPadding, kFocusBorderPadding, |
| kFocusBorderPadding, kFocusBorderPadding); |
| } |
| return insets; |
| } |
| |
| int Label::GetBaseline() const { |
| return GetInsets().top() + font_list().GetBaseline(); |
| } |
| |
| gfx::Size Label::GetPreferredSize() const { |
| // Return a size of (0, 0) if the label is not visible and if the |
| // |collapse_when_hidden_| flag is set. |
| // TODO(munjal): This logic probably belongs to the View class. But for now, |
| // put it here since putting it in View class means all inheriting classes |
| // need to respect the |collapse_when_hidden_| flag. |
| if (!visible() && collapse_when_hidden_) |
| return gfx::Size(); |
| |
| if (multi_line() && max_width_ != 0 && !text().empty()) |
| return gfx::Size(max_width_, GetHeightForWidth(max_width_)); |
| |
| gfx::Size size(GetTextSize()); |
| const gfx::Insets insets = GetInsets(); |
| size.Enlarge(insets.width(), insets.height()); |
| return size; |
| } |
| |
| gfx::Size Label::GetMinimumSize() const { |
| if (!visible() && collapse_when_hidden_) |
| return gfx::Size(); |
| |
| gfx::Size size(0, font_list().GetHeight()); |
| if (elide_behavior_ == gfx::ELIDE_HEAD || |
| elide_behavior_ == gfx::ELIDE_MIDDLE || |
| elide_behavior_ == gfx::ELIDE_TAIL || |
| elide_behavior_ == gfx::ELIDE_EMAIL) { |
| size.set_width(gfx::Canvas::GetStringWidth( |
| base::string16(gfx::kEllipsisUTF16), font_list())); |
| } |
| if (!multi_line()) |
| size.SetToMin(GetTextSize()); |
| size.Enlarge(GetInsets().width(), GetInsets().height()); |
| return size; |
| } |
| |
| int Label::GetHeightForWidth(int w) const { |
| if (!visible() && collapse_when_hidden_) |
| return 0; |
| |
| w -= GetInsets().width(); |
| int height = 0; |
| if (!multi_line() || text().empty() || w <= 0) { |
| height = std::max(line_height(), font_list().GetHeight()); |
| } else if (render_text_->MultilineSupported()) { |
| // SetDisplayRect() has a side effect for later calls of GetStringSize(). |
| // Be careful to invoke |render_text_->SetDisplayRect(gfx::Rect())| to |
| // cancel this effect before the next time GetStringSize() is called. |
| // It would be beneficial not to cancel here, considering that some layout |
| // managers invoke GetHeightForWidth() for the same width multiple times |
| // and |render_text_| can cache the height. |
| render_text_->SetDisplayRect(gfx::Rect(0, 0, w, 0)); |
| height = render_text_->GetStringSize().height(); |
| } else { |
| std::vector<base::string16> lines = GetLinesForWidth(w); |
| height = lines.size() * std::max(line_height(), font_list().GetHeight()); |
| } |
| height -= gfx::ShadowValue::GetMargin(render_text_->shadows()).height(); |
| return height + GetInsets().height(); |
| } |
| |
| void Label::Layout() { |
| lines_.clear(); |
| } |
| |
| const char* Label::GetClassName() const { |
| return kViewClassName; |
| } |
| |
| View* Label::GetTooltipHandlerForPoint(const gfx::Point& point) { |
| if (!handles_tooltips_ || |
| (tooltip_text_.empty() && !ShouldShowDefaultTooltip())) |
| return NULL; |
| |
| return HitTestPoint(point) ? this : NULL; |
| } |
| |
| bool Label::CanProcessEventsWithinSubtree() const { |
| // Send events to the parent view for handling. |
| return false; |
| } |
| |
| void Label::GetAccessibleState(ui::AXViewState* state) { |
| state->role = ui::AX_ROLE_STATIC_TEXT; |
| state->AddStateFlag(ui::AX_STATE_READ_ONLY); |
| // Note that |render_text_| is never elided (see the comment in Init() too). |
| state->name = render_text_->GetDisplayText(); |
| } |
| |
| bool Label::GetTooltipText(const gfx::Point& p, base::string16* tooltip) const { |
| if (!handles_tooltips_) |
| return false; |
| |
| if (!tooltip_text_.empty()) { |
| tooltip->assign(tooltip_text_); |
| return true; |
| } |
| |
| if (ShouldShowDefaultTooltip()) { |
| // Note that |render_text_| is never elided (see the comment in Init() too). |
| tooltip->assign(render_text_->GetDisplayText()); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void Label::OnEnabledChanged() { |
| RecalculateColors(); |
| } |
| |
| scoped_ptr<gfx::RenderText> Label::CreateRenderText( |
| const base::string16& text, |
| gfx::HorizontalAlignment alignment, |
| gfx::DirectionalityMode directionality, |
| gfx::ElideBehavior elide_behavior) { |
| scoped_ptr<gfx::RenderText> render_text( |
| render_text_->CreateInstanceOfSameType()); |
| render_text->SetHorizontalAlignment(alignment); |
| render_text->SetDirectionalityMode(directionality); |
| render_text->SetElideBehavior(elide_behavior); |
| render_text->SetObscured(obscured()); |
| render_text->SetMinLineHeight(line_height()); |
| render_text->SetFontList(font_list()); |
| render_text->set_shadows(shadows()); |
| render_text->SetCursorEnabled(false); |
| render_text->SetText(text); |
| return render_text.Pass(); |
| } |
| |
| void Label::PaintText(gfx::Canvas* canvas) { |
| MaybeBuildRenderTextLines(); |
| for (size_t i = 0; i < lines_.size(); ++i) |
| lines_[i]->Draw(canvas); |
| } |
| |
| void Label::OnBoundsChanged(const gfx::Rect& previous_bounds) { |
| if (previous_bounds.size() != size()) |
| InvalidateLayout(); |
| } |
| |
| void Label::OnPaint(gfx::Canvas* canvas) { |
| View::OnPaint(canvas); |
| if (is_first_paint_text_) { |
| // TODO(ckocagil): Remove ScopedTracker below once crbug.com/441028 is |
| // fixed. |
| tracked_objects::ScopedTracker tracking_profile( |
| FROM_HERE_WITH_EXPLICIT_FUNCTION("441028 First PaintText()")); |
| |
| is_first_paint_text_ = false; |
| PaintText(canvas); |
| } else { |
| PaintText(canvas); |
| } |
| if (HasFocus()) |
| canvas->DrawFocusRect(GetFocusBounds()); |
| } |
| |
| void Label::OnNativeThemeChanged(const ui::NativeTheme* theme) { |
| UpdateColorsFromTheme(theme); |
| } |
| |
| void Label::OnDeviceScaleFactorChanged(float device_scale_factor) { |
| View::OnDeviceScaleFactorChanged(device_scale_factor); |
| // When the device scale factor is changed, some font rendering parameters is |
| // changed (especially, hinting). The bounding box of the text has to be |
| // re-computed based on the new parameters. See crbug.com/441439 |
| ResetLayout(); |
| } |
| |
| void Label::VisibilityChanged(View* starting_from, bool is_visible) { |
| if (!is_visible) |
| lines_.clear(); |
| } |
| |
| void Label::Init(const base::string16& text, const gfx::FontList& font_list) { |
| render_text_.reset(gfx::RenderText::CreateInstance()); |
| render_text_->SetHorizontalAlignment(gfx::ALIGN_CENTER); |
| render_text_->SetDirectionalityMode(gfx::DIRECTIONALITY_FROM_TEXT); |
| // NOTE: |render_text_| should not be elided at all. This is used to keep some |
| // properties and to compute the size of the string. |
| render_text_->SetElideBehavior(gfx::NO_ELIDE); |
| render_text_->SetFontList(font_list); |
| render_text_->SetCursorEnabled(false); |
| render_text_->SetWordWrapBehavior(gfx::TRUNCATE_LONG_WORDS); |
| |
| elide_behavior_ = gfx::ELIDE_TAIL; |
| enabled_color_set_ = disabled_color_set_ = background_color_set_ = false; |
| subpixel_rendering_enabled_ = true; |
| auto_color_readability_ = true; |
| multi_line_ = false; |
| UpdateColorsFromTheme(GetNativeTheme()); |
| handles_tooltips_ = true; |
| collapse_when_hidden_ = false; |
| max_width_ = 0; |
| is_first_paint_text_ = true; |
| SetText(text); |
| } |
| |
| void Label::ResetLayout() { |
| InvalidateLayout(); |
| PreferredSizeChanged(); |
| SchedulePaint(); |
| lines_.clear(); |
| } |
| |
| void Label::MaybeBuildRenderTextLines() { |
| if (!lines_.empty()) |
| return; |
| |
| gfx::Rect rect = GetContentsBounds(); |
| if (focusable()) |
| rect.Inset(kFocusBorderPadding, kFocusBorderPadding); |
| if (rect.IsEmpty()) |
| return; |
| |
| gfx::HorizontalAlignment alignment = horizontal_alignment(); |
| gfx::DirectionalityMode directionality = render_text_->directionality_mode(); |
| if (multi_line()) { |
| // Force the directionality and alignment of the first line on other lines. |
| bool rtl = |
| render_text_->GetDisplayTextDirection() == base::i18n::RIGHT_TO_LEFT; |
| if (alignment == gfx::ALIGN_TO_HEAD) |
| alignment = rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT; |
| directionality = |
| rtl ? gfx::DIRECTIONALITY_FORCE_RTL : gfx::DIRECTIONALITY_FORCE_LTR; |
| } |
| |
| // Text eliding is not supported for multi-lined Labels. |
| // TODO(mukai): Add multi-lined elided text support. |
| gfx::ElideBehavior elide_behavior = |
| multi_line() ? gfx::NO_ELIDE : elide_behavior_; |
| if (!multi_line() || render_text_->MultilineSupported()) { |
| scoped_ptr<gfx::RenderText> render_text = |
| CreateRenderText(text(), alignment, directionality, elide_behavior); |
| render_text->SetDisplayRect(rect); |
| render_text->SetMultiline(multi_line()); |
| render_text->SetWordWrapBehavior(render_text_->word_wrap_behavior()); |
| lines_.push_back(render_text.Pass()); |
| } else { |
| std::vector<base::string16> lines = GetLinesForWidth(rect.width()); |
| if (lines.size() > 1) |
| rect.set_height(std::max(line_height(), font_list().GetHeight())); |
| |
| const int bottom = GetContentsBounds().bottom(); |
| for (size_t i = 0; i < lines.size() && rect.y() <= bottom; ++i) { |
| scoped_ptr<gfx::RenderText> line = |
| CreateRenderText(lines[i], alignment, directionality, elide_behavior); |
| line->SetDisplayRect(rect); |
| lines_.push_back(line.Pass()); |
| rect.set_y(rect.y() + rect.height()); |
| } |
| // Append the remaining text to the last visible line. |
| for (size_t i = lines_.size(); i < lines.size(); ++i) |
| lines_.back()->SetText(lines_.back()->text() + lines[i]); |
| } |
| RecalculateColors(); |
| } |
| |
| gfx::Rect Label::GetFocusBounds() { |
| MaybeBuildRenderTextLines(); |
| |
| gfx::Rect focus_bounds; |
| if (lines_.empty()) { |
| focus_bounds = gfx::Rect(GetTextSize()); |
| } else { |
| for (size_t i = 0; i < lines_.size(); ++i) { |
| gfx::Point origin; |
| origin += lines_[i]->GetLineOffset(0); |
| focus_bounds.Union(gfx::Rect(origin, lines_[i]->GetStringSize())); |
| } |
| } |
| |
| focus_bounds.Inset(-kFocusBorderPadding, -kFocusBorderPadding); |
| focus_bounds.Intersect(GetLocalBounds()); |
| return focus_bounds; |
| } |
| |
| std::vector<base::string16> Label::GetLinesForWidth(int width) const { |
| std::vector<base::string16> lines; |
| // |width| can be 0 when getting the default text size, in that case |
| // the ideal lines (i.e. broken at newline characters) are wanted. |
| if (width <= 0) { |
| lines = base::SplitString( |
| render_text_->GetDisplayText(), base::string16(1, '\n'), |
| base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); |
| } else { |
| gfx::ElideRectangleText(render_text_->GetDisplayText(), font_list(), width, |
| std::numeric_limits<int>::max(), |
| render_text_->word_wrap_behavior(), &lines); |
| } |
| return lines; |
| } |
| |
| gfx::Size Label::GetTextSize() const { |
| gfx::Size size; |
| if (text().empty()) { |
| size = gfx::Size(0, std::max(line_height(), font_list().GetHeight())); |
| } else if (!multi_line() || render_text_->MultilineSupported()) { |
| // Cancel the display rect of |render_text_|. The display rect may be |
| // specified in GetHeightForWidth(), and specifying empty Rect cancels |
| // its effect. See also the comment in GetHeightForWidth(). |
| // TODO(mukai): use gfx::Rect() to compute the ideal size rather than |
| // the current width(). See crbug.com/468494, crbug.com/467526, and |
| // the comment for MultilinePreferredSizeTest in label_unittest.cc. |
| render_text_->SetDisplayRect(gfx::Rect(0, 0, width(), 0)); |
| size = render_text_->GetStringSize(); |
| } else { |
| // Get the natural text size, unelided and only wrapped on newlines. |
| std::vector<base::string16> lines = GetLinesForWidth(width()); |
| scoped_ptr<gfx::RenderText> render_text(gfx::RenderText::CreateInstance()); |
| render_text->SetFontList(font_list()); |
| for (size_t i = 0; i < lines.size(); ++i) { |
| render_text->SetText(lines[i]); |
| const gfx::Size line = render_text->GetStringSize(); |
| size.set_width(std::max(size.width(), line.width())); |
| size.set_height(std::max(line_height(), size.height() + line.height())); |
| } |
| } |
| const gfx::Insets shadow_margin = -gfx::ShadowValue::GetMargin(shadows()); |
| size.Enlarge(shadow_margin.width(), shadow_margin.height()); |
| return size; |
| } |
| |
| void Label::RecalculateColors() { |
| actual_enabled_color_ = auto_color_readability_ ? |
| color_utils::GetReadableColor(requested_enabled_color_, |
| background_color_) : |
| requested_enabled_color_; |
| actual_disabled_color_ = auto_color_readability_ ? |
| color_utils::GetReadableColor(requested_disabled_color_, |
| background_color_) : |
| requested_disabled_color_; |
| |
| SkColor color = enabled() ? actual_enabled_color_ : actual_disabled_color_; |
| bool subpixel_rendering_suppressed = |
| SkColorGetA(background_color_) != 0xFF || !subpixel_rendering_enabled_; |
| for (size_t i = 0; i < lines_.size(); ++i) { |
| lines_[i]->SetColor(color); |
| lines_[i]->set_subpixel_rendering_suppressed(subpixel_rendering_suppressed); |
| } |
| SchedulePaint(); |
| } |
| |
| void Label::UpdateColorsFromTheme(const ui::NativeTheme* theme) { |
| if (!enabled_color_set_) { |
| requested_enabled_color_ = theme->GetSystemColor( |
| ui::NativeTheme::kColorId_LabelEnabledColor); |
| } |
| if (!disabled_color_set_) { |
| requested_disabled_color_ = theme->GetSystemColor( |
| ui::NativeTheme::kColorId_LabelDisabledColor); |
| } |
| if (!background_color_set_) { |
| background_color_ = theme->GetSystemColor( |
| ui::NativeTheme::kColorId_LabelBackgroundColor); |
| } |
| RecalculateColors(); |
| } |
| |
| bool Label::ShouldShowDefaultTooltip() const { |
| const gfx::Size text_size = GetTextSize(); |
| const gfx::Size size = GetContentsBounds().size(); |
| return !obscured() && (text_size.width() > size.width() || |
| (multi_line() && text_size.height() > size.height())); |
| } |
| |
| } // namespace views |