diff options
author | estade@chromium.org <estade@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-03-15 22:56:28 +0000 |
---|---|---|
committer | estade@chromium.org <estade@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-03-15 22:56:28 +0000 |
commit | d2878a95999f6917758e0d24e9c387ace639c01a (patch) | |
tree | 4fc57885b6a86bd6f157ed6f3dc2fc07d74b43f3 /ui | |
parent | 39dda93df8a7039e297d4e82053f701afb06627e (diff) | |
download | chromium_src-d2878a95999f6917758e0d24e9c387ace639c01a.zip chromium_src-d2878a95999f6917758e0d24e9c387ace639c01a.tar.gz chromium_src-d2878a95999f6917758e0d24e9c387ace639c01a.tar.bz2 |
Add views::RichLabel, a class which creates multi-line text with mixed
links and plain text.
BUG=168704
Review URL: https://codereview.chromium.org/12543032
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@188496 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'ui')
-rw-r--r-- | ui/views/controls/link_listener.h | 4 | ||||
-rw-r--r-- | ui/views/controls/styled_label.cc | 173 | ||||
-rw-r--r-- | ui/views/controls/styled_label.h | 79 | ||||
-rw-r--r-- | ui/views/controls/styled_label_listener.h | 26 | ||||
-rw-r--r-- | ui/views/controls/styled_label_unittest.cc | 114 | ||||
-rw-r--r-- | ui/views/views.gyp | 4 | ||||
-rw-r--r-- | ui/views/window/dialog_client_view.cc | 2 |
7 files changed, 400 insertions, 2 deletions
diff --git a/ui/views/controls/link_listener.h b/ui/views/controls/link_listener.h index 67162b0..99a6077 100644 --- a/ui/views/controls/link_listener.h +++ b/ui/views/controls/link_listener.h @@ -5,12 +5,14 @@ #ifndef UI_VIEWS_CONTROLS_LINK_LISTENER_H_ #define UI_VIEWS_CONTROLS_LINK_LISTENER_H_ +#include "ui/views/views_export.h" + namespace views { class Link; // An interface implemented by an object to let it know that a link was clicked. -class LinkListener { +class VIEWS_EXPORT LinkListener { public: virtual void LinkClicked(Link* source, int event_flags) = 0; diff --git a/ui/views/controls/styled_label.cc b/ui/views/controls/styled_label.cc new file mode 100644 index 0000000..f875c7f --- /dev/null +++ b/ui/views/controls/styled_label.cc @@ -0,0 +1,173 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ui/views/controls/styled_label.h" + +#include <vector> + +#include "base/string_util.h" +#include "ui/base/text/text_elider.h" +#include "ui/views/controls/label.h" +#include "ui/views/controls/link.h" +#include "ui/views/controls/styled_label_listener.h" + +namespace views { + +namespace { + +// Calculates the height of a line of text. Currently returns the height of +// a label. +int CalculateLineHeight() { + Label label; + return label.GetPreferredSize().height(); +} + +} // namespace + +bool StyledLabel::LinkRange::operator<( + const StyledLabel::LinkRange& other) const { + // Intentionally reversed so the priority queue is sorted by smallest first. + return range.start() > other.range.start(); +} + +StyledLabel::StyledLabel(const string16& text, StyledLabelListener* listener) + : text_(text), + listener_(listener) {} + +StyledLabel::~StyledLabel() {} + +void StyledLabel::AddLink(const ui::Range& range) { + DCHECK(!range.is_reversed()); + DCHECK(!range.is_empty()); + DCHECK(ui::Range(0, text_.size()).Contains(range)); + link_ranges_.push(LinkRange(range)); + calculated_size_ = gfx::Size(); + InvalidateLayout(); +} + +gfx::Insets StyledLabel::GetInsets() const { + gfx::Insets insets = View::GetInsets(); + const gfx::Insets focus_border_padding(1, 1, 1, 1); + insets += focus_border_padding; + return insets; +} + +int StyledLabel::GetHeightForWidth(int w) { + if (w != calculated_size_.width()) + calculated_size_ = gfx::Size(w, CalculateAndDoLayout(w, true)); + + return calculated_size_.height(); +} + +void StyledLabel::Layout() { + CalculateAndDoLayout(GetLocalBounds().width(), false); +} + +void StyledLabel::LinkClicked(Link* source, int event_flags) { + listener_->StyledLabelLinkClicked(link_targets_[source], event_flags); +} + +int StyledLabel::CalculateAndDoLayout(int width, bool dry_run) { + if (!dry_run) { + RemoveAllChildViews(true); + link_targets_.clear(); + } + + width -= GetInsets().width(); + if (width <= 0) + return 0; + + const int line_height = CalculateLineHeight(); + // The index of the line we're on. + int line = 0; + // The x position (in pixels) of the line we're on, relative to content + // bounds. + int x = 0; + + string16 remaining_string = text_; + std::priority_queue<LinkRange> link_ranges = link_ranges_; + + // Iterate over the text, creating a bunch of labels and links and laying them + // out in the appropriate positions. + while (!remaining_string.empty()) { + // Don't put whitespace at beginning of a line. + if (x == 0) + TrimWhitespace(remaining_string, TRIM_LEADING, &remaining_string); + + ui::Range range(ui::Range::InvalidRange()); + if (!link_ranges.empty()) + range = link_ranges.top().range; + + const gfx::Rect chunk_bounds(x, 0, width - x, 2 * line_height); + std::vector<string16> substrings; + ui::ElideRectangleText(remaining_string, + gfx::Font(), + chunk_bounds.width(), + chunk_bounds.height(), + ui::IGNORE_LONG_WORDS, + &substrings); + + string16 chunk = substrings[0]; + if (chunk.empty()) { + // Nothing fit on this line. Start a new line. If x is 0, there's no room + // for anything. Just abort. + if (x == 0) + break; + + x = 0; + line++; + continue; + } + + scoped_ptr<View> view; + const size_t position = text_.size() - remaining_string.size(); + if (position >= range.start()) { + // This chunk is a link. + if (chunk.size() < range.length() && x != 0) { + // Don't wrap links. Try to fit them entirely on one line. + x = 0; + line++; + continue; + } + + chunk = chunk.substr(0, range.length()); + Link* link = new Link(chunk); + link->set_listener(this); + if (!dry_run) + link_targets_[link] = range; + view.reset(link); + link_ranges.pop(); + } else { + // This chunk is normal text. + if (position + chunk.size() > range.start()) + chunk = chunk.substr(0, range.start() - position); + + Label* label = new Label(chunk); + // Give the label a focus border so that its preferred size matches + // links' preferred sizes. + label->SetHasFocusBorder(true); + view.reset(label); + } + + // Lay out the views to overlap by 1 pixel to compensate for their border + // spacing. Otherwise, "<a>link</a>," will render as "link ,". + const int overlap = 1; + const gfx::Size view_size = view->GetPreferredSize(); + DCHECK_EQ(line_height, view_size.height() - 2 * overlap); + if (!dry_run) { + view->SetBoundsRect(gfx::Rect( + gfx::Point(GetInsets().left() + x - overlap, + GetInsets().top() + line * line_height - overlap), + view_size)); + AddChildView(view.release()); + } + x += view_size.width() - 2 * overlap; + + remaining_string = remaining_string.substr(chunk.size()); + } + + return (line + 1) * line_height + GetInsets().height(); +} + +} // namespace views diff --git a/ui/views/controls/styled_label.h b/ui/views/controls/styled_label.h new file mode 100644 index 0000000..3b683d2 --- /dev/null +++ b/ui/views/controls/styled_label.h @@ -0,0 +1,79 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UI_VIEWS_CONTROLS_STYLED_LABEL_H_ +#define UI_VIEWS_CONTROLS_STYLED_LABEL_H_ + +#include <map> +#include <queue> + +#include "base/basictypes.h" +#include "base/string16.h" +#include "ui/base/range/range.h" +#include "ui/gfx/size.h" +#include "ui/views/controls/link_listener.h" +#include "ui/views/view.h" + +namespace views { + +class Link; +class StyledLabelListener; + +// A class which can apply mixed styles to a block of text. Currently, text is +// always multiline, and the only style that may be applied is linkifying ranges +// of text. +class VIEWS_EXPORT StyledLabel : public View, public LinkListener { + public: + StyledLabel(const string16& text, StyledLabelListener* listener); + virtual ~StyledLabel(); + + // Marks the given range within |text_| as a link. + void AddLink(const ui::Range& range); + + // View implementation: + virtual gfx::Insets GetInsets() const OVERRIDE; + virtual int GetHeightForWidth(int w) OVERRIDE; + virtual void Layout() OVERRIDE; + + // LinkListener implementation: + virtual void LinkClicked(Link* source, int event_flags) OVERRIDE; + + private: + struct LinkRange { + explicit LinkRange(const ui::Range& range) : range(range) {} + ~LinkRange() {} + + bool operator<(const LinkRange& other) const; + + ui::Range range; + }; + + // Calculates how to layout child views, creates them and sets their size + // and position. |width| is the horizontal space, in pixels, that the view + // has to work with. If |dry_run| is true, the view hierarchy is not touched. + // The return value is the height in pixels. + int CalculateAndDoLayout(int width, bool dry_run); + + // The text to display. + string16 text_; + + // The listener that will be informed of link clicks. + StyledLabelListener* listener_; + + // The ranges that should be linkified, sorted by start position. + std::priority_queue<LinkRange> link_ranges_; + + // A mapping from Link* control to the range it corresponds to in |text_|. + std::map<Link*, ui::Range> link_targets_; + + // This variable saves the result of the last GetHeightForWidth call in order + // to avoid repeated calculation. + gfx::Size calculated_size_; + + DISALLOW_COPY_AND_ASSIGN(StyledLabel); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_STYLED_LABEL_H_ diff --git a/ui/views/controls/styled_label_listener.h b/ui/views/controls/styled_label_listener.h new file mode 100644 index 0000000..e04c299 --- /dev/null +++ b/ui/views/controls/styled_label_listener.h @@ -0,0 +1,26 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UI_VIEWS_CONTROLS_STYLED_LABEL_LISTENER_H_ +#define UI_VIEWS_CONTROLS_STYLED_LABEL_LISTENER_H_ + +namespace ui { +class Range; +} + +namespace views { + +// A listener interface for StyledLabel. +class VIEWS_EXPORT StyledLabelListener { + public: + virtual void StyledLabelLinkClicked(const ui::Range& range, + int event_flags) = 0; + + protected: + virtual ~StyledLabelListener() {} +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_STYLED_LABEL_LISTENER_H_ diff --git a/ui/views/controls/styled_label_unittest.cc b/ui/views/controls/styled_label_unittest.cc new file mode 100644 index 0000000..e3462b2 --- /dev/null +++ b/ui/views/controls/styled_label_unittest.cc @@ -0,0 +1,114 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <string> + +#include "base/basictypes.h" +#include "base/memory/scoped_ptr.h" +#include "base/utf_string_conversions.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/views/controls/link.h" +#include "ui/views/controls/styled_label.h" +#include "ui/views/controls/styled_label_listener.h" + +namespace views { + +class StyledLabelTest : public testing::Test, public StyledLabelListener { + public: + StyledLabelTest() {} + virtual ~StyledLabelTest() {} + + // StyledLabelListener implementation. + virtual void StyledLabelLinkClicked(const ui::Range& range, + int event_flags) OVERRIDE {} + + protected: + StyledLabel* styled() { return styled_.get(); } + + void InitStyledLabel(const std::string& ascii_text) { + styled_.reset(new StyledLabel(ASCIIToUTF16(ascii_text), this)); + } + + int StyledLabelContentHeightForWidth(int w) { + return styled_->GetHeightForWidth(w) - styled_->GetInsets().height(); + } + + private: + scoped_ptr<StyledLabel> styled_; + + DISALLOW_COPY_AND_ASSIGN(StyledLabelTest); +}; + +TEST_F(StyledLabelTest, NoWrapping) { + const std::string text("This is a test block of text"); + InitStyledLabel(text); + Label label(ASCIIToUTF16(text)); + const gfx::Size label_preferred_size = label.GetPreferredSize(); + EXPECT_EQ(label_preferred_size.height(), + StyledLabelContentHeightForWidth(label_preferred_size.width() * 2)); +} + +TEST_F(StyledLabelTest, BasicWrapping) { + const std::string text("This is a test block of text"); + InitStyledLabel(text); + Label label(ASCIIToUTF16(text.substr(0, text.size() * 2 / 3))); + gfx::Size label_preferred_size = label.GetPreferredSize(); + EXPECT_EQ(label_preferred_size.height() * 2, + StyledLabelContentHeightForWidth(label_preferred_size.width())); + + // Also respect the border. + styled()->set_border(Border::CreateEmptyBorder(3, 3, 3, 3)); + styled()->SetBounds( + 0, + 0, + styled()->GetInsets().width() + label_preferred_size.width(), + styled()->GetInsets().height() + 2 * label_preferred_size.height()); + styled()->Layout(); + ASSERT_EQ(2, styled()->child_count()); + EXPECT_EQ(3, styled()->child_at(0)->bounds().x()); + EXPECT_EQ(3, styled()->child_at(0)->bounds().y()); + EXPECT_EQ(styled()->bounds().height() - 3, + styled()->child_at(1)->bounds().bottom()); +} + +TEST_F(StyledLabelTest, CreateLinks) { + const std::string text("This is a test block of text."); + InitStyledLabel(text); + styled()->AddLink(ui::Range(0, 1)); + styled()->AddLink(ui::Range(1, 2)); + styled()->AddLink(ui::Range(10, 11)); + styled()->AddLink(ui::Range(12, 13)); + + styled()->SetBounds(0, 0, 1000, 1000); + styled()->Layout(); + ASSERT_EQ(7, styled()->child_count()); +} + +TEST_F(StyledLabelTest, DontBreakLinks) { + const std::string text("This is a test block of text, "); + const std::string link_text("and this should be a link"); + InitStyledLabel(text + link_text); + styled()->AddLink(ui::Range(text.size(), text.size() + link_text.size())); + + Label label(ASCIIToUTF16(text + link_text.substr(0, link_text.size() / 2))); + gfx::Size label_preferred_size = label.GetPreferredSize(); + int pref_height = styled()->GetHeightForWidth(label_preferred_size.width()); + EXPECT_EQ(label_preferred_size.height() * 2, + pref_height - styled()->GetInsets().height()); + + styled()->SetBounds(0, 0, label_preferred_size.width(), pref_height); + styled()->Layout(); + ASSERT_EQ(2, styled()->child_count()); + EXPECT_EQ(0, styled()->child_at(0)->bounds().x()); + EXPECT_EQ(0, styled()->child_at(1)->bounds().x()); +} + +TEST_F(StyledLabelTest, HandleEmptyLayout) { + const std::string text("This is a test block of text, "); + InitStyledLabel(text); + styled()->Layout(); + ASSERT_EQ(0, styled()->child_count()); +} + +} // namespace diff --git a/ui/views/views.gyp b/ui/views/views.gyp index e4b1cc8e..1b2c43a 100644 --- a/ui/views/views.gyp +++ b/ui/views/views.gyp @@ -197,6 +197,9 @@ 'controls/slide_out_view.h', 'controls/slider.cc', 'controls/slider.h', + 'controls/styled_label.cc', + 'controls/styled_label.h', + 'controls/styled_label_listener.h', 'controls/tabbed_pane/tabbed_pane.cc', 'controls/tabbed_pane/tabbed_pane.h', 'controls/tabbed_pane/tabbed_pane_listener.h', @@ -674,6 +677,7 @@ 'controls/scroll_view_unittest.cc', 'controls/single_split_view_unittest.cc', 'controls/slider_unittest.cc', + 'controls/styled_label_unittest.cc', 'controls/tabbed_pane/tabbed_pane_unittest.cc', 'controls/table/table_utils_unittest.cc', 'controls/table/table_view_unittest.cc', diff --git a/ui/views/window/dialog_client_view.cc b/ui/views/window/dialog_client_view.cc index 5a3e503..e3b97c6 100644 --- a/ui/views/window/dialog_client_view.cc +++ b/ui/views/window/dialog_client_view.cc @@ -182,7 +182,7 @@ void DialogClientView::Layout() { // Layout the footnote view. if (footnote_view_) { - const int height = footnote_view_->GetPreferredSize().height(); + const int height = footnote_view_->GetHeightForWidth(bounds.width()); footnote_view_->SetBounds(bounds.x(), bounds.bottom() - height, bounds.width(), height); bounds.Inset(0, 0, 0, height + kRelatedControlVerticalSpacing); |