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