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