summaryrefslogtreecommitdiffstats
path: root/ui
diff options
context:
space:
mode:
authorestade@chromium.org <estade@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-03-15 22:56:28 +0000
committerestade@chromium.org <estade@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-03-15 22:56:28 +0000
commitd2878a95999f6917758e0d24e9c387ace639c01a (patch)
tree4fc57885b6a86bd6f157ed6f3dc2fc07d74b43f3 /ui
parent39dda93df8a7039e297d4e82053f701afb06627e (diff)
downloadchromium_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.h4
-rw-r--r--ui/views/controls/styled_label.cc173
-rw-r--r--ui/views/controls/styled_label.h79
-rw-r--r--ui/views/controls/styled_label_listener.h26
-rw-r--r--ui/views/controls/styled_label_unittest.cc114
-rw-r--r--ui/views/views.gyp4
-rw-r--r--ui/views/window/dialog_client_view.cc2
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);