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