bounded_label.cc revision a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7
1// Copyright 2013 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/message_center/views/bounded_label.h"
6
7#include <limits>
8
9#include "base/strings/string_util.h"
10#include "base/strings/utf_string_conversions.h"
11#include "ui/gfx/canvas.h"
12#include "ui/gfx/text_elider.h"
13#include "ui/views/controls/label.h"
14
15namespace {
16
17const size_t kPreferredLinesCacheSize = 10;
18
19}  // namespace
20
21namespace message_center {
22
23// InnerBoundedLabel ///////////////////////////////////////////////////////////
24
25// InnerBoundedLabel is a views::Label subclass that does all of the work for
26// BoundedLabel. It is kept private to prevent outside code from calling a
27// number of views::Label methods like setFont() that break BoundedLabel's
28// caching but can't be overridden.
29//
30// TODO(dharcourt): Move the line limiting functionality to views::Label to make
31// this unnecessary.
32
33class InnerBoundedLabel : public views::Label {
34 public:
35  InnerBoundedLabel(const BoundedLabel& owner);
36  virtual ~InnerBoundedLabel();
37
38  void SetNativeTheme(const ui::NativeTheme* theme);
39
40  // Pass in a -1 width to use the preferred width, a -1 limit to skip limits.
41  int GetLinesForWidthAndLimit(int width, int limit);
42  gfx::Size GetSizeForWidthAndLines(int width, int lines);
43  std::vector<string16> GetWrappedText(int width, int lines);
44
45 protected:
46  // Overridden from views::Label.
47  virtual void OnBoundsChanged(const gfx::Rect& previous_bounds) OVERRIDE;
48  virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE;
49
50 private:
51  int GetTextFlags();
52
53  void ClearCaches();
54  int GetCachedLines(int width);
55  void SetCachedLines(int width, int lines);
56  gfx::Size GetCachedSize(const std::pair<int, int>& width_and_lines);
57  void SetCachedSize(std::pair<int, int> width_and_lines, gfx::Size size);
58
59  const BoundedLabel* owner_;  // Weak reference.
60  string16 wrapped_text_;
61  int wrapped_text_width_;
62  int wrapped_text_lines_;
63  std::map<int, int> lines_cache_;
64  std::list<int> lines_widths_;  // Most recently used in front.
65  std::map<std::pair<int, int>, gfx::Size> size_cache_;
66  std::list<std::pair<int, int> > size_widths_and_lines_;  // Recent in front.
67
68  DISALLOW_COPY_AND_ASSIGN(InnerBoundedLabel);
69};
70
71InnerBoundedLabel::InnerBoundedLabel(const BoundedLabel& owner)
72    : owner_(&owner),
73      wrapped_text_width_(0),
74      wrapped_text_lines_(0) {
75  SetMultiLine(true);
76  SetAllowCharacterBreak(true);
77  SetHorizontalAlignment(gfx::ALIGN_LEFT);
78  set_collapse_when_hidden(true);
79}
80
81InnerBoundedLabel::~InnerBoundedLabel() {
82}
83
84void InnerBoundedLabel::SetNativeTheme(const ui::NativeTheme* theme) {
85  ClearCaches();
86  OnNativeThemeChanged(theme);
87}
88
89int InnerBoundedLabel::GetLinesForWidthAndLimit(int width, int limit) {
90  if (width == 0 || limit == 0)
91    return 0;
92  int lines = GetCachedLines(width);
93  if (lines == std::numeric_limits<int>::max()) {
94    int text_width = std::max(width - owner_->GetInsets().width(), 0);
95    lines = GetWrappedText(text_width, lines).size();
96    SetCachedLines(width, lines);
97  }
98  return (limit < 0 || lines <= limit) ? lines : limit;
99}
100
101gfx::Size InnerBoundedLabel::GetSizeForWidthAndLines(int width, int lines) {
102  if (width == 0 || lines == 0)
103    return gfx::Size();
104  std::pair<int, int> key(width, lines);
105  gfx::Size size = GetCachedSize(key);
106  if (size.height() == std::numeric_limits<int>::max()) {
107    gfx::Insets insets = owner_->GetInsets();
108    int text_width = (width < 0) ? std::numeric_limits<int>::max() :
109                                   std::max(width - insets.width(), 0);
110    int text_height = std::numeric_limits<int>::max();
111    std::vector<string16> wrapped = GetWrappedText(text_width, lines);
112    gfx::Canvas::SizeStringInt(JoinString(wrapped, '\n'), font(),
113                               &text_width, &text_height,
114                               owner_->GetLineHeight(),
115                               GetTextFlags());
116    size.set_width(text_width + insets.width());
117    size.set_height(text_height + insets.height());
118    SetCachedSize(key, size);
119  }
120  return size;
121}
122
123std::vector<string16> InnerBoundedLabel::GetWrappedText(int width, int lines) {
124  // Short circuit simple case.
125  if (width == 0 || lines == 0)
126    return std::vector<string16>();
127
128  // Restrict line limit to ensure (lines + 1) * line_height <= INT_MAX and
129  // use it to calculate a reasonable text height.
130  int height = std::numeric_limits<int>::max();
131  if (lines > 0) {
132    int line_height = std::max(font().GetHeight(), 2);  // At least 2 pixels.
133    int max_lines = std::numeric_limits<int>::max() / line_height - 1;
134    lines = std::min(lines, max_lines);
135    height = (lines + 1) * line_height;
136  }
137
138  // Try to ensure that the width is no smaller than the width of the text's
139  // characters to avoid the http://crbug.com/237700 infinite loop.
140  // TODO(dharcourt): Remove when http://crbug.com/237700 is fixed.
141  width = std::max(width, 2 * font().GetStringWidth(UTF8ToUTF16("W")));
142
143  // Wrap, using INT_MAX for -1 widths that indicate no wrapping.
144  std::vector<string16> wrapped;
145  gfx::ElideRectangleText(text(), font_list(),
146                          (width < 0) ? std::numeric_limits<int>::max() : width,
147                          height, gfx::WRAP_LONG_WORDS, &wrapped);
148
149  // Elide if necessary.
150  if (lines > 0 && wrapped.size() > static_cast<unsigned int>(lines)) {
151    // Add an ellipsis to the last line. If this ellipsis makes the last line
152    // too wide, that line will be further elided by the gfx::ElideText below,
153    // so for example "ABC" could become "ABC..." and then "AB...".
154    string16 last = wrapped[lines - 1] + UTF8ToUTF16(gfx::kEllipsis);
155    if (width > 0 && font().GetStringWidth(last) > width)
156      last = gfx::ElideText(last, font(), width, gfx::ELIDE_AT_END);
157    wrapped.resize(lines - 1);
158    wrapped.push_back(last);
159  }
160
161  return wrapped;
162}
163
164void InnerBoundedLabel::OnBoundsChanged(const gfx::Rect& previous_bounds) {
165  ClearCaches();
166  views::Label::OnBoundsChanged(previous_bounds);
167}
168
169void InnerBoundedLabel::OnPaint(gfx::Canvas* canvas) {
170  views::Label::OnPaintBackground(canvas);
171  views::Label::OnPaintBorder(canvas);
172  int lines = owner_->GetLineLimit();
173  int height = GetSizeForWidthAndLines(width(), lines).height();
174  if (height > 0) {
175    gfx::Rect bounds(width(), height);
176    bounds.Inset(owner_->GetInsets());
177    if (bounds.width() != wrapped_text_width_ || lines != wrapped_text_lines_) {
178      wrapped_text_ = JoinString(GetWrappedText(bounds.width(), lines), '\n');
179      wrapped_text_width_ = bounds.width();
180      wrapped_text_lines_ = lines;
181    }
182    bounds.set_x(GetMirroredXForRect(bounds));
183    PaintText(canvas, wrapped_text_, bounds, GetTextFlags());
184  }
185}
186
187int InnerBoundedLabel::GetTextFlags() {
188  int flags = gfx::Canvas::MULTI_LINE | gfx::Canvas::CHARACTER_BREAK;
189
190  // We can't use subpixel rendering if the background is non-opaque.
191  if (SkColorGetA(background_color()) != 0xFF)
192    flags |= gfx::Canvas::NO_SUBPIXEL_RENDERING;
193
194  if (directionality_mode() ==
195      views::Label::AUTO_DETECT_DIRECTIONALITY) {
196    base::i18n::TextDirection direction =
197        base::i18n::GetFirstStrongCharacterDirection(text());
198    if (direction == base::i18n::RIGHT_TO_LEFT)
199      flags |= gfx::Canvas::FORCE_RTL_DIRECTIONALITY;
200    else
201      flags |= gfx::Canvas::FORCE_LTR_DIRECTIONALITY;
202  }
203
204  return flags;
205}
206
207void InnerBoundedLabel::ClearCaches() {
208  wrapped_text_width_ = 0;
209  wrapped_text_lines_ = 0;
210  lines_cache_.clear();
211  lines_widths_.clear();
212  size_cache_.clear();
213  size_widths_and_lines_.clear();
214}
215
216int InnerBoundedLabel::GetCachedLines(int width) {
217  int lines = std::numeric_limits<int>::max();
218  std::map<int, int>::const_iterator found;
219  if ((found = lines_cache_.find(width)) != lines_cache_.end()) {
220    lines = found->second;
221    lines_widths_.remove(width);
222    lines_widths_.push_front(width);
223  }
224  return lines;
225}
226
227void InnerBoundedLabel::SetCachedLines(int width, int lines) {
228  if (lines_cache_.size() >= kPreferredLinesCacheSize) {
229    lines_cache_.erase(lines_widths_.back());
230    lines_widths_.pop_back();
231  }
232  lines_cache_[width] = lines;
233  lines_widths_.push_front(width);
234}
235
236gfx::Size InnerBoundedLabel::GetCachedSize(
237    const std::pair<int, int>& width_and_lines) {
238  gfx::Size size(width_and_lines.first, std::numeric_limits<int>::max());
239  std::map<std::pair<int, int>, gfx::Size>::const_iterator found;
240  if ((found = size_cache_.find(width_and_lines)) != size_cache_.end()) {
241    size = found->second;
242    size_widths_and_lines_.remove(width_and_lines);
243    size_widths_and_lines_.push_front(width_and_lines);
244  }
245  return size;
246}
247
248void InnerBoundedLabel::SetCachedSize(std::pair<int, int> width_and_lines,
249                                      gfx::Size size) {
250  if (size_cache_.size() >= kPreferredLinesCacheSize) {
251    size_cache_.erase(size_widths_and_lines_.back());
252    size_widths_and_lines_.pop_back();
253  }
254  size_cache_[width_and_lines] = size;
255  size_widths_and_lines_.push_front(width_and_lines);
256}
257
258// BoundedLabel ///////////////////////////////////////////////////////////
259
260BoundedLabel::BoundedLabel(const string16& text, const gfx::FontList& font_list)
261    : line_limit_(-1) {
262  label_.reset(new InnerBoundedLabel(*this));
263  label_->SetFontList(font_list);
264  label_->SetText(text);
265}
266
267BoundedLabel::BoundedLabel(const string16& text)
268    : line_limit_(-1) {
269  label_.reset(new InnerBoundedLabel(*this));
270  label_->SetText(text);
271}
272
273BoundedLabel::~BoundedLabel() {
274}
275
276void BoundedLabel::SetColors(SkColor textColor, SkColor backgroundColor) {
277  label_->SetEnabledColor(textColor);
278  label_->SetBackgroundColor(backgroundColor);
279}
280
281void BoundedLabel::SetLineHeight(int height) {
282  label_->SetLineHeight(height);
283}
284
285void BoundedLabel::SetLineLimit(int lines) {
286  line_limit_ = std::max(lines, -1);
287}
288
289int BoundedLabel::GetLineHeight() const {
290  return label_->line_height();
291}
292
293int BoundedLabel::GetLineLimit() const {
294  return line_limit_;
295}
296
297int BoundedLabel::GetLinesForWidthAndLimit(int width, int limit) {
298  return visible() ? label_->GetLinesForWidthAndLimit(width, limit) : 0;
299}
300
301gfx::Size BoundedLabel::GetSizeForWidthAndLines(int width, int lines) {
302  return visible() ?
303         label_->GetSizeForWidthAndLines(width, lines) : gfx::Size();
304}
305
306int BoundedLabel::GetBaseline() const {
307  return label_->GetBaseline();
308}
309
310gfx::Size BoundedLabel::GetPreferredSize() {
311  return visible() ? label_->GetSizeForWidthAndLines(-1, -1) : gfx::Size();
312}
313
314int BoundedLabel::GetHeightForWidth(int width) {
315  return visible() ?
316         label_->GetSizeForWidthAndLines(width, line_limit_).height() : 0;
317}
318
319void BoundedLabel::Paint(gfx::Canvas* canvas) {
320  if (visible())
321    label_->Paint(canvas);
322}
323
324bool BoundedLabel::HitTestRect(const gfx::Rect& rect) const {
325  return label_->HitTestRect(rect);
326}
327
328void BoundedLabel::GetAccessibleState(ui::AccessibleViewState* state) {
329  label_->GetAccessibleState(state);
330}
331
332void BoundedLabel::OnBoundsChanged(const gfx::Rect& previous_bounds) {
333  label_->SetBoundsRect(bounds());
334  views::View::OnBoundsChanged(previous_bounds);
335}
336
337void BoundedLabel::OnNativeThemeChanged(const ui::NativeTheme* theme) {
338  label_->SetNativeTheme(theme);
339}
340
341string16 BoundedLabel::GetWrappedTextForTest(int width, int lines) {
342  return JoinString(label_->GetWrappedText(width, lines), '\n');
343}
344
345}  // namespace message_center
346