blob: 08f43237f0052c464f58145936c31b7b1afbd051 [file] [log] [blame]
// Copyright (c) 2012 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/message_center/views/notification_view.h"
#include <stddef.h>
#include "base/command_line.h"
#include "base/macros.h"
#include "base/strings/string_util.h"
#include "build/build_config.h"
#include "components/url_formatter/elide_url.h"
#include "third_party/skia/include/core/SkPaint.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/base/cursor/cursor.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/layout.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/skia_util.h"
#include "ui/gfx/text_elider.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/message_center_style.h"
#include "ui/message_center/public/cpp/message_center_constants.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_types.h"
#include "ui/message_center/views/bounded_label.h"
#include "ui/message_center/views/notification_button.h"
#include "ui/message_center/views/notification_control_buttons_view.h"
#include "ui/message_center/views/padded_button.h"
#include "ui/message_center/views/proportional_image_view.h"
#include "ui/native_theme/native_theme.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/progress_bar.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/native_cursor.h"
#include "ui/views/painter.h"
#include "ui/views/widget/widget.h"
#include "url/gurl.h"
namespace message_center {
namespace {
const int kTextBottomPadding = 12;
const int kItemTitleToMessagePadding = 3;
// Character limit = pixels per line * line limit / min. pixels per character.
const int kMinPixelsPerTitleCharacter = 4;
constexpr size_t kMessageCharacterLimit =
kNotificationWidth * kMessageExpandedLineLimit / 3;
constexpr size_t kContextMessageCharacterLimit =
kNotificationWidth * kContextMessageLineLimit / 3;
// Dimensions.
const int kProgressBarBottomPadding = 0;
// static
std::unique_ptr<views::Border> MakeEmptyBorder(int top,
int left,
int bottom,
int right) {
return views::CreateEmptyBorder(top, left, bottom, right);
}
// static
std::unique_ptr<views::Border> MakeTextBorder(int padding,
int top,
int bottom) {
// Split the padding between the top and the bottom, then add the extra space.
return MakeEmptyBorder(padding / 2 + top, kTextLeftPadding,
(padding + 1) / 2 + bottom, kTextRightPadding);
}
// static
std::unique_ptr<views::Border> MakeProgressBarBorder(int top, int bottom) {
return MakeEmptyBorder(top, kTextLeftPadding, bottom, kTextRightPadding);
}
// static
std::unique_ptr<views::Border> MakeSeparatorBorder(int top,
int left,
SkColor color) {
return views::CreateSolidSidedBorder(top, left, 0, 0, color);
}
#if !defined(OS_CHROMEOS)
// static
void SetBorderRight(views::View* view, int right) {
const gfx::Insets& insets = view->GetInsets();
if (insets.right() != right) {
view->SetBorder(
MakeEmptyBorder(insets.top(), insets.left(), insets.bottom(), right));
}
}
#endif
// NotificationItemView ////////////////////////////////////////////////////////
// NotificationItemViews are responsible for drawing each list notification
// item's title and message next to each other within a single column.
class NotificationItemView : public views::View {
public:
explicit NotificationItemView(const NotificationItem& item);
private:
DISALLOW_COPY_AND_ASSIGN(NotificationItemView);
};
NotificationItemView::NotificationItemView(const NotificationItem& item) {
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::kHorizontal, gfx::Insets(),
kItemTitleToMessagePadding));
views::Label* title = new views::Label(item.title);
title->set_collapse_when_hidden(true);
title->SetHorizontalAlignment(gfx::ALIGN_LEFT);
title->SetEnabledColor(kRegularTextColor);
title->SetAutoColorReadabilityEnabled(false);
AddChildView(title);
views::Label* message = new views::Label(item.message);
message->set_collapse_when_hidden(true);
message->SetHorizontalAlignment(gfx::ALIGN_LEFT);
message->SetEnabledColor(kDimTextColor);
message->SetAutoColorReadabilityEnabled(false);
AddChildView(message);
PreferredSizeChanged();
SchedulePaint();
}
} // namespace
// NotificationView ////////////////////////////////////////////////////////////
void NotificationView::CreateOrUpdateViews(const Notification& notification) {
top_view_count_ = 0;
CreateOrUpdateTitleView(notification);
CreateOrUpdateMessageView(notification);
CreateOrUpdateProgressBarView(notification);
CreateOrUpdateListItemViews(notification);
CreateOrUpdateIconView(notification);
CreateOrUpdateSmallIconView(notification);
CreateOrUpdateImageView(notification);
CreateOrUpdateContextMessageView(notification);
CreateOrUpdateActionButtonViews(notification);
}
NotificationView::NotificationView(const Notification& notification)
: MessageView(notification) {
// Create the top_view_, which collects into a vertical box all content
// at the top of the notification (to the right of the icon) except for the
// close button.
top_view_ = new views::View();
top_view_->SetLayoutManager(
std::make_unique<views::BoxLayout>(views::BoxLayout::kVertical));
top_view_->SetBorder(
MakeEmptyBorder(kTextTopPadding - 8, 0, kTextBottomPadding - 5, 0));
AddChildView(top_view_);
// Create the bottom_view_, which collects into a vertical box all content
// below the notification icon.
bottom_view_ = new views::View();
bottom_view_->SetLayoutManager(
std::make_unique<views::BoxLayout>(views::BoxLayout::kVertical));
AddChildView(bottom_view_);
control_buttons_view_ = new NotificationControlButtonsView(this);
AddChildView(control_buttons_view_);
views::ImageView* small_image_view = new views::ImageView();
small_image_view->SetImageSize(gfx::Size(kSmallImageSize, kSmallImageSize));
small_image_view->set_owned_by_client();
small_image_view_.reset(small_image_view);
CreateOrUpdateViews(notification);
// Put together the different content and control views. Layering those allows
// for proper layout logic and it also allows the control buttons and small
// image to overlap the content as needed to provide large enough click and
// touch areas (<http://crbug.com/168822> and <http://crbug.com/168856>).
AddChildView(small_image_view_.get());
UpdateControlButtonsVisibilityWithNotification(notification);
set_notify_enter_exit_on_child(true);
}
NotificationView::~NotificationView() {
}
gfx::Size NotificationView::CalculatePreferredSize() const {
int top_width = top_view_->GetPreferredSize().width() +
icon_view_->GetPreferredSize().width();
int bottom_width = bottom_view_->GetPreferredSize().width();
int preferred_width = std::max(top_width, bottom_width) + GetInsets().width();
return gfx::Size(preferred_width, GetHeightForWidth(preferred_width));
}
int NotificationView::GetHeightForWidth(int width) const {
// Get the height assuming no line limit changes.
int content_width = width - GetInsets().width();
int top_height = top_view_->GetHeightForWidth(content_width);
int bottom_height = bottom_view_->GetHeightForWidth(content_width);
// <http://crbug.com/230448> Fix: Adjust the height when the message_view's
// line limit would be different for the specified width than it currently is.
// TODO(dharcourt): Avoid BoxLayout and directly compute the correct height.
if (message_view_) {
int title_lines = 0;
if (title_view_) {
title_lines = title_view_->GetLinesForWidthAndLimit(width,
kMaxTitleLines);
}
int used_limit = message_view_->GetLineLimit();
int correct_limit = GetMessageLineLimit(title_lines, width);
if (used_limit != correct_limit) {
top_height -= GetMessageHeight(content_width, used_limit);
top_height += GetMessageHeight(content_width, correct_limit);
}
}
int content_height =
std::max(top_height, kNotificationIconSize) + bottom_height;
// Adjust the height to make sure there is at least 16px of space below the
// icon if there is any space there (<http://crbug.com/232966>).
if (content_height > kNotificationIconSize) {
content_height =
std::max(content_height, kNotificationIconSize + kIconBottomPadding);
}
return content_height + GetInsets().height();
}
void NotificationView::Layout() {
// |ShrinkTopmostLabel| updates the borders of views to make space for the
// control buttons. We have to update the borders before calling
// |MessageView::Layout| so that the latest values get taken into account
// while layouting. As |ShrinkTopmostLabel| only depends on the PreferredSize,
// this is valid to do before calling |MessageView::Layout|.
ShrinkTopmostLabel();
MessageView::Layout();
gfx::Insets insets = GetInsets();
int content_width = width() - insets.width();
gfx::Rect content_bounds = GetContentsBounds();
// Before any resizing, set or adjust the number of message lines.
int title_lines = 0;
if (title_view_) {
title_lines =
title_view_->GetLinesForWidthAndLimit(width(), kMaxTitleLines);
}
if (message_view_)
message_view_->SetLineLimit(GetMessageLineLimit(title_lines, width()));
// Top views.
int top_height = top_view_->GetHeightForWidth(content_width);
top_view_->SetBounds(insets.left(), insets.top(), content_width, top_height);
// Icon.
icon_view_->SetBounds(insets.left(), insets.top(), kNotificationIconSize,
kNotificationIconSize);
// Control buttons (close and settings buttons).
gfx::Rect control_buttons_bounds(content_bounds);
int buttons_width = control_buttons_view_->GetPreferredSize().width();
int buttons_height = control_buttons_view_->GetPreferredSize().height();
control_buttons_bounds.set_x(control_buttons_bounds.right() - buttons_width -
kControlButtonPadding);
control_buttons_bounds.set_y(control_buttons_bounds.y() +
kControlButtonPadding);
control_buttons_bounds.set_width(buttons_width);
control_buttons_bounds.set_height(buttons_height);
control_buttons_view_->SetBoundsRect(control_buttons_bounds);
// Small icon.
gfx::Size small_image_size(small_image_view_->GetPreferredSize());
gfx::Rect small_image_rect(small_image_size);
small_image_rect.set_origin(gfx::Point(
content_bounds.right() - small_image_size.width() - kSmallImagePadding,
content_bounds.bottom() - small_image_size.height() -
kSmallImagePadding));
small_image_view_->SetBoundsRect(small_image_rect);
// Bottom views.
int bottom_y = insets.top() + std::max(top_height, kNotificationIconSize);
int bottom_height = bottom_view_->GetHeightForWidth(content_width);
bottom_view_->SetBounds(insets.left(), bottom_y,
content_width, bottom_height);
}
void NotificationView::OnFocus() {
MessageView::OnFocus();
ScrollRectToVisible(GetLocalBounds());
}
void NotificationView::ScrollRectToVisible(const gfx::Rect& rect) {
// Notification want to show the whole notification when a part of it (like
// a button) gets focused.
views::View::ScrollRectToVisible(GetLocalBounds());
}
void NotificationView::OnMouseEntered(const ui::MouseEvent& event) {
MessageView::OnMouseEntered(event);
UpdateControlButtonsVisibility();
}
void NotificationView::OnMouseExited(const ui::MouseEvent& event) {
MessageView::OnMouseExited(event);
UpdateControlButtonsVisibility();
}
void NotificationView::UpdateWithNotification(
const Notification& notification) {
MessageView::UpdateWithNotification(notification);
CreateOrUpdateViews(notification);
UpdateControlButtonsVisibilityWithNotification(notification);
Layout();
SchedulePaint();
}
void NotificationView::ButtonPressed(views::Button* sender,
const ui::Event& event) {
// Certain operations can cause |this| to be destructed, so copy the members
// we send to other parts of the code.
// TODO(dewittj): Remove this hack.
std::string id(notification_id());
// See if the button pressed was an action button.
for (size_t i = 0; i < action_buttons_.size(); ++i) {
if (sender == action_buttons_[i]) {
MessageCenter::Get()->ClickOnNotificationButton(id, i);
return;
}
}
NOTREACHED();
}
void NotificationView::CreateOrUpdateTitleView(
const Notification& notification) {
if (notification.title().empty()) {
// Deletion will also remove |title_view_| from its parent.
delete title_view_;
title_view_ = nullptr;
return;
}
DCHECK(top_view_);
const gfx::FontList& font_list =
views::Label().font_list().DeriveWithSizeDelta(2);
constexpr int kTitleCharacterLimit =
kNotificationWidth * kMaxTitleLines / kMinPixelsPerTitleCharacter;
base::string16 title = gfx::TruncateString(
notification.title(), kTitleCharacterLimit, gfx::WORD_BREAK);
if (!title_view_) {
int padding = kTitleLineHeight - font_list.GetHeight();
title_view_ = new BoundedLabel(title, font_list);
title_view_->SetLineHeight(kTitleLineHeight);
title_view_->SetLineLimit(kMaxTitleLines);
title_view_->SetColor(kRegularTextColor);
title_view_->SetBorder(MakeTextBorder(padding, 3, 0));
top_view_->AddChildViewAt(title_view_, top_view_count_);
} else {
title_view_->SetText(title);
}
top_view_count_++;
}
void NotificationView::CreateOrUpdateMessageView(
const Notification& notification) {
if (notification.message().empty()) {
// Deletion will also remove |message_view_| from its parent.
delete message_view_;
message_view_ = nullptr;
return;
}
DCHECK(top_view_ != NULL);
base::string16 text = gfx::TruncateString(notification.message(),
kMessageCharacterLimit,
gfx::WORD_BREAK);
if (!message_view_) {
int padding = kMessageLineHeight - views::Label().font_list().GetHeight();
message_view_ = new BoundedLabel(text);
message_view_->SetLineHeight(kMessageLineHeight);
message_view_->SetColor(kRegularTextColor);
message_view_->SetBorder(MakeTextBorder(padding, 4, 0));
top_view_->AddChildViewAt(message_view_, top_view_count_);
} else {
message_view_->SetText(text);
}
message_view_->SetVisible(notification.items().empty());
top_view_count_++;
}
base::string16 NotificationView::FormatContextMessage(
const Notification& notification) const {
if (notification.UseOriginAsContextMessage()) {
const GURL url = notification.origin_url();
DCHECK(url.is_valid());
return gfx::ElideText(
url_formatter::FormatUrlForSecurityDisplay(
url, url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS),
views::Label().font_list(), kContextMessageViewWidth, gfx::ELIDE_HEAD);
}
return gfx::TruncateString(notification.context_message(),
kContextMessageCharacterLimit, gfx::WORD_BREAK);
}
void NotificationView::CreateOrUpdateContextMessageView(
const Notification& notification) {
if (notification.context_message().empty() &&
!notification.UseOriginAsContextMessage()) {
// Deletion will also remove |context_message_view_| from its parent.
delete context_message_view_;
context_message_view_ = nullptr;
return;
}
DCHECK(top_view_ != NULL);
base::string16 message = FormatContextMessage(notification);
if (!context_message_view_) {
int padding = kMessageLineHeight - views::Label().font_list().GetHeight();
context_message_view_ = new BoundedLabel(message);
context_message_view_->SetLineLimit(kContextMessageLineLimit);
context_message_view_->SetLineHeight(kMessageLineHeight);
context_message_view_->SetColor(kDimTextColor);
context_message_view_->SetBorder(MakeTextBorder(padding, 4, 0));
top_view_->AddChildViewAt(context_message_view_, top_view_count_);
} else {
context_message_view_->SetText(message);
}
top_view_count_++;
}
void NotificationView::CreateOrUpdateProgressBarView(
const Notification& notification) {
if (notification.type() != NOTIFICATION_TYPE_PROGRESS) {
// Deletion will also remove |progress_bar_view_| from its parent.
delete progress_bar_view_;
progress_bar_view_ = nullptr;
return;
}
DCHECK(top_view_);
if (!progress_bar_view_) {
progress_bar_view_ = new views::ProgressBar();
progress_bar_view_->SetBorder(MakeProgressBarBorder(
kProgressBarTopPadding, kProgressBarBottomPadding));
top_view_->AddChildViewAt(progress_bar_view_, top_view_count_);
}
progress_bar_view_->SetValue(notification.progress() / 100.0);
progress_bar_view_->SetVisible(notification.items().empty());
top_view_count_++;
}
void NotificationView::CreateOrUpdateListItemViews(
const Notification& notification) {
for (auto* item_view : item_views_)
delete item_view;
item_views_.clear();
int padding = kMessageLineHeight - views::Label().font_list().GetHeight();
std::vector<NotificationItem> items = notification.items();
if (items.size() == 0)
return;
DCHECK(top_view_);
for (size_t i = 0; i < items.size() && i < kNotificationMaximumItems; ++i) {
NotificationItemView* item_view = new NotificationItemView(items[i]);
item_view->SetBorder(MakeTextBorder(padding, i ? 0 : 4, 0));
item_views_.push_back(item_view);
top_view_->AddChildViewAt(item_view, top_view_count_++);
}
}
void NotificationView::CreateOrUpdateIconView(
const Notification& notification) {
gfx::Size image_view_size(kNotificationIconSize, kNotificationIconSize);
if (!icon_view_) {
icon_view_ = new ProportionalImageView(image_view_size);
AddChildView(icon_view_);
}
gfx::ImageSkia icon = notification.icon().AsImageSkia();
icon_view_->SetImage(icon, icon.size());
}
void NotificationView::CreateOrUpdateSmallIconView(
const Notification& notification) {
small_image_view_->SetImage(notification.small_image().AsImageSkia());
}
void NotificationView::CreateOrUpdateImageView(
const Notification& notification) {
// |image_view_| is the view representing the area covered by the
// notification's image, including background and border. Its size can be
// specified in advance and images will be scaled to fit including a border if
// necessary.
if (notification.image().IsEmpty()) {
delete image_container_;
image_container_ = NULL;
image_view_ = NULL;
return;
}
gfx::Size ideal_size(kNotificationPreferredImageWidth,
kNotificationPreferredImageHeight);
if (!image_container_) {
DCHECK(!image_view_);
DCHECK(bottom_view_);
DCHECK_EQ(this, bottom_view_->parent());
image_container_ = new views::View();
image_container_->SetLayoutManager(std::make_unique<views::FillLayout>());
image_container_->SetBackground(
views::CreateSolidBackground(kImageBackgroundColor));
image_view_ = new ProportionalImageView(ideal_size);
image_container_->AddChildView(image_view_);
bottom_view_->AddChildViewAt(image_container_, 0);
}
DCHECK(image_view_);
image_view_->SetImage(notification.image().AsImageSkia(), ideal_size);
gfx::Size scaled_size =
GetImageSizeForContainerSize(ideal_size, notification.image().Size());
image_view_->SetBorder(
ideal_size != scaled_size
? views::CreateSolidBorder(kNotificationImageBorderSize,
SK_ColorTRANSPARENT)
: NULL);
}
void NotificationView::CreateOrUpdateActionButtonViews(
const Notification& notification) {
std::vector<ButtonInfo> buttons = notification.buttons();
bool new_buttons = action_buttons_.size() != buttons.size();
if (new_buttons || buttons.size() == 0) {
for (auto* item : separators_)
delete item;
separators_.clear();
for (auto* item : action_buttons_)
delete item;
action_buttons_.clear();
}
DCHECK(bottom_view_);
DCHECK_EQ(this, bottom_view_->parent());
for (size_t i = 0; i < buttons.size(); ++i) {
ButtonInfo button_info = buttons[i];
if (new_buttons) {
views::View* separator = new views::ImageView();
separator->SetBorder(MakeSeparatorBorder(1, 0, kButtonSeparatorColor));
separators_.push_back(separator);
bottom_view_->AddChildView(separator);
NotificationButton* button = new NotificationButton(this);
button->SetTitle(button_info.title);
button->SetIcon(button_info.icon.AsImageSkia());
action_buttons_.push_back(button);
bottom_view_->AddChildView(button);
} else {
action_buttons_[i]->SetTitle(button_info.title);
action_buttons_[i]->SetIcon(button_info.icon.AsImageSkia());
action_buttons_[i]->SchedulePaint();
action_buttons_[i]->Layout();
}
}
if (new_buttons) {
Layout();
views::Widget* widget = GetWidget();
if (widget != NULL) {
widget->SetSize(widget->GetContentsView()->GetPreferredSize());
GetWidget()->SynthesizeMouseMoveEvent();
}
}
}
void NotificationView::UpdateControlButtonsVisibilityWithNotification(
const Notification& notification) {
control_buttons_view_->ShowSettingsButton(
notification.should_show_settings_button());
control_buttons_view_->ShowCloseButton(GetMode() ==
MessageView::Mode::NORMAL);
UpdateControlButtonsVisibility();
}
NotificationControlButtonsView* NotificationView::GetControlButtonsView()
const {
return control_buttons_view_;
}
int NotificationView::GetMessageLineLimit(int title_lines, int width) const {
int effective_title_lines = std::max(0, title_lines - 1);
int line_reduction_from_title = 2 * effective_title_lines;
// Title lines are counted as twice as big as message lines for the purpose
// of this calculation.
// The effect from the title reduction here should be:
// * 0 title lines: 5 max lines message.
// * 1 title line: 5 max lines message.
// * 2 title lines: 3 max lines message.
return std::max(0, kMessageExpandedLineLimit - line_reduction_from_title);
}
int NotificationView::GetMessageHeight(int width, int limit) const {
return message_view_ ?
message_view_->GetSizeForWidthAndLines(width, limit).height() : 0;
}
void NotificationView::ShrinkTopmostLabel() {
// Reduce width of the topmost label not to be covered by the control buttons
// only on non Chrome OS platform.
#if !defined(OS_CHROMEOS)
const int child_count = top_view_->child_count();
if (child_count > 0) {
const int buttons_width = control_buttons_view_->GetPreferredSize().width();
SetBorderRight(top_view_->child_at(0), kTextRightPadding + buttons_width);
for (int i = 1; i < child_count; ++i)
SetBorderRight(top_view_->child_at(i), kTextRightPadding);
}
#endif
}
} // namespace message_center