blob: ae2d9283c6626c46861ab13fc9ed03cbece54211 [file] [log] [blame]
// Copyright 2017 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_header_view.h"
#include <memory>
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/message_center/public/cpp/message_center_constants.h"
#include "ui/message_center/vector_icons.h"
#include "ui/message_center/views/relative_time_formatter.h"
#include "ui/strings/grit/ui_strings.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/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_types.h"
#include "ui/views/painter.h"
#include "ui/views/view_class_properties.h"
namespace message_center {
namespace {
constexpr int kHeaderHeight = 32;
// The padding between controls in the header.
constexpr gfx::Insets kHeaderSpacing(0, 2, 0, 2);
// The padding outer the header and the control buttons.
constexpr gfx::Insets kHeaderOuterPadding(2, 2, 0, 2);
constexpr int kInnerHeaderHeight = kHeaderHeight - kHeaderOuterPadding.height();
// Default paddings of the views of texts. Adjusted on Windows.
// Top: 9px = 11px (from the mock) - 2px (outer padding).
// Buttom: 6px from the mock.
constexpr gfx::Insets kTextViewPaddingDefault(9, 0, 6, 0);
// Paddings of the app icon (small image).
// Top: 8px = 10px (from the mock) - 2px (outer padding).
// Bottom: 4px from the mock.
// Right: 4px = 6px (from the mock) - kHeaderHorizontalSpacing.
constexpr gfx::Insets kAppIconPadding(8, 14, 4, 4);
// Size of the expand icon. 8px = 32px - 15px - 9px (values from the mock).
constexpr int kExpandIconSize = 8;
// Paddings of the expand buttons.
// Top: 13px = 15px (from the mock) - 2px (outer padding).
// Bottom: 9px from the mock.
constexpr gfx::Insets kExpandIconViewPadding(13, 2, 9, 0);
// Bullet character. The divider symbol between different parts of the header.
constexpr wchar_t kNotificationHeaderDivider[] = L" \u2022 ";
// "Roboto-Regular, 12sp" is specified in the mock.
constexpr int kHeaderTextFontSize = 12;
// Minimum spacing before the control buttons.
constexpr int kControlButtonSpacing = 16;
// ExpandButtton forwards all mouse and key events to NotificationHeaderView,
// but takes tab focus for accessibility purpose.
class ExpandButton : public views::ImageView {
public:
ExpandButton();
~ExpandButton() override;
// Overridden from views::ImageView:
void OnPaint(gfx::Canvas* canvas) override;
void OnFocus() override;
void OnBlur() override;
void GetAccessibleNodeData(ui::AXNodeData* node_data) override;
private:
std::unique_ptr<views::Painter> focus_painter_;
};
ExpandButton::ExpandButton() {
focus_painter_ = views::Painter::CreateSolidFocusPainter(
kFocusBorderColor, gfx::Insets(0, 0, 1, 1));
SetFocusBehavior(FocusBehavior::ALWAYS);
}
ExpandButton::~ExpandButton() = default;
void ExpandButton::OnPaint(gfx::Canvas* canvas) {
views::ImageView::OnPaint(canvas);
if (HasFocus())
views::Painter::PaintPainterAt(canvas, focus_painter_.get(),
GetContentsBounds());
}
void ExpandButton::OnFocus() {
views::ImageView::OnFocus();
SchedulePaint();
}
void ExpandButton::OnBlur() {
views::ImageView::OnBlur();
SchedulePaint();
}
void ExpandButton::GetAccessibleNodeData(ui::AXNodeData* node_data) {
node_data->role = ax::mojom::Role::kButton;
node_data->SetName(GetTooltipText(gfx::Point()));
}
gfx::FontList GetHeaderTextFontList() {
gfx::Font default_font;
int font_size_delta = kHeaderTextFontSize - default_font.GetFontSize();
gfx::Font font = default_font.Derive(font_size_delta, gfx::Font::NORMAL,
gfx::Font::Weight::NORMAL);
DCHECK_EQ(kHeaderTextFontSize, font.GetFontSize());
return gfx::FontList(font);
}
gfx::Insets CalculateTopPadding(int font_list_height) {
#if defined(OS_WIN)
// On Windows, the fonts can have slightly different metrics reported,
// depending on where the code runs. In Chrome, DirectWrite is on, which means
// font metrics are reported from Skia, which rounds from float using ceil.
// In unit tests, however, GDI is used to report metrics, and the height
// reported there is consistent with other platforms. This means there is a
// difference of 1px in height between Chrome on Windows and everything else
// (where everything else includes unit tests on Windows). This 1px causes the
// text and everything else to stop aligning correctly, so we account for it
// by shrinking the top padding by 1.
if (font_list_height != 15) {
DCHECK_EQ(16, font_list_height);
return kTextViewPaddingDefault - gfx::Insets(1 /* top */, 0, 0, 0);
}
#endif
DCHECK_EQ(15, font_list_height);
return kTextViewPaddingDefault;
}
} // namespace
NotificationHeaderView::NotificationHeaderView(views::ButtonListener* listener)
: views::Button(listener) {
const views::FlexSpecification kAppNameFlex =
views::FlexSpecification::ForSizeRule(
views::MinimumFlexSizeRule::kScaleToZero,
views::MaximumFlexSizeRule::kPreferred)
.WithOrder(1);
const views::FlexSpecification kSpacerFlex =
views::FlexSpecification::ForSizeRule(
views::MinimumFlexSizeRule::kScaleToMinimum,
views::MaximumFlexSizeRule::kUnbounded)
.WithOrder(2);
auto* layout = SetLayoutManager(std::make_unique<views::FlexLayout>());
layout->SetDefault(views::kMarginsKey, kHeaderSpacing);
layout->SetInteriorMargin(kHeaderOuterPadding);
layout->SetCollapseMargins(true);
// App icon view
app_icon_view_ = new views::ImageView();
app_icon_view_->SetImageSize(gfx::Size(kSmallImageSizeMD, kSmallImageSizeMD));
app_icon_view_->SetBorder(views::CreateEmptyBorder(kAppIconPadding));
app_icon_view_->SetVerticalAlignment(views::ImageView::Alignment::kLeading);
app_icon_view_->SetHorizontalAlignment(views::ImageView::Alignment::kLeading);
DCHECK_EQ(kInnerHeaderHeight, app_icon_view_->GetPreferredSize().height());
AddChildView(app_icon_view_);
// Font list for text views.
gfx::FontList font_list = GetHeaderTextFontList();
const int font_list_height = font_list.GetHeight();
gfx::Insets text_view_padding(CalculateTopPadding(font_list_height));
auto create_label = [&font_list, font_list_height, text_view_padding]() {
auto* label = new views::Label();
label->SetFontList(font_list);
label->SetLineHeight(font_list_height);
label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
label->SetBorder(views::CreateEmptyBorder(text_view_padding));
DCHECK_EQ(kInnerHeaderHeight, label->GetPreferredSize().height());
return label;
};
// App name view
app_name_view_ = create_label();
// Explicitly disable multiline to support proper text elision for URLs.
app_name_view_->SetMultiLine(false);
app_name_view_->SetProperty(views::kFlexBehaviorKey, kAppNameFlex);
AddChildView(app_name_view_);
// Detail views which will be hidden in settings mode.
detail_views_ = new views::View();
auto* detail_layout =
detail_views_->SetLayoutManager(std::make_unique<views::FlexLayout>());
detail_layout->SetCollapseMargins(true);
detail_layout->SetDefault(views::kMarginsKey, kHeaderSpacing);
AddChildView(detail_views_);
// Summary text divider
summary_text_divider_ = create_label();
summary_text_divider_->SetText(base::WideToUTF16(kNotificationHeaderDivider));
summary_text_divider_->SetVisible(false);
detail_views_->AddChildView(summary_text_divider_);
// Summary text view
summary_text_view_ = create_label();
summary_text_view_->SetVisible(false);
detail_views_->AddChildView(summary_text_view_);
// Timestamp divider
timestamp_divider_ = create_label();
timestamp_divider_->SetText(base::WideToUTF16(kNotificationHeaderDivider));
timestamp_divider_->SetVisible(false);
detail_views_->AddChildView(timestamp_divider_);
// Timestamp view
timestamp_view_ = create_label();
timestamp_view_->SetVisible(false);
detail_views_->AddChildView(timestamp_view_);
// Expand button view
expand_button_ = new ExpandButton();
expand_button_->SetBorder(views::CreateEmptyBorder(kExpandIconViewPadding));
expand_button_->SetVerticalAlignment(views::ImageView::Alignment::kLeading);
expand_button_->SetHorizontalAlignment(views::ImageView::Alignment::kLeading);
expand_button_->SetImageSize(gfx::Size(kExpandIconSize, kExpandIconSize));
DCHECK_EQ(kInnerHeaderHeight, expand_button_->GetPreferredSize().height());
detail_views_->AddChildView(expand_button_);
// Spacer between left-aligned views and right-aligned views
views::View* spacer = new views::View;
spacer->SetPreferredSize(
gfx::Size(kControlButtonSpacing, kInnerHeaderHeight));
spacer->SetProperty(views::kFlexBehaviorKey, kSpacerFlex);
AddChildView(spacer);
SetAccentColor(accent_color_);
SetPreferredSize(gfx::Size(kNotificationWidth, kHeaderHeight));
}
NotificationHeaderView::~NotificationHeaderView() = default;
void NotificationHeaderView::SetAppIcon(const gfx::ImageSkia& img) {
app_icon_view_->SetImage(img);
using_default_app_icon_ = false;
}
void NotificationHeaderView::ClearAppIcon() {
app_icon_view_->SetImage(
gfx::CreateVectorIcon(kProductIcon, kSmallImageSizeMD, accent_color_));
using_default_app_icon_ = true;
}
void NotificationHeaderView::SetAppName(const base::string16& name) {
app_name_view_->SetText(name);
}
void NotificationHeaderView::SetAppNameElideBehavior(
gfx::ElideBehavior elide_behavior) {
app_name_view_->SetElideBehavior(elide_behavior);
}
void NotificationHeaderView::SetProgress(int progress) {
summary_text_view_->SetText(l10n_util::GetStringFUTF16Int(
IDS_MESSAGE_CENTER_NOTIFICATION_PROGRESS_PERCENTAGE, progress));
has_progress_ = true;
UpdateSummaryTextVisibility();
}
void NotificationHeaderView::SetSummaryText(const base::string16& text) {
DCHECK(!has_progress_);
summary_text_view_->SetText(text);
UpdateSummaryTextVisibility();
}
void NotificationHeaderView::ClearProgress() {
summary_text_view_->SetText(base::string16());
has_progress_ = false;
UpdateSummaryTextVisibility();
}
void NotificationHeaderView::SetOverflowIndicator(int count) {
if (count > 0) {
summary_text_view_->SetText(l10n_util::GetStringFUTF16Int(
IDS_MESSAGE_CENTER_LIST_NOTIFICATION_HEADER_OVERFLOW_INDICATOR, count));
} else {
summary_text_view_->SetText(base::string16());
}
UpdateSummaryTextVisibility();
}
void NotificationHeaderView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
Button::GetAccessibleNodeData(node_data);
node_data->SetName(app_name_view_->GetText());
node_data->SetDescription(summary_text_view_->GetText() +
base::ASCIIToUTF16(" ") +
timestamp_view_->GetText());
if (is_expanded_)
node_data->AddState(ax::mojom::State::kExpanded);
}
void NotificationHeaderView::SetTimestamp(base::Time timestamp) {
base::string16 relative_time;
base::TimeDelta next_update;
GetRelativeTimeStringAndNextUpdateTime(timestamp - base::Time::Now(),
&relative_time, &next_update);
timestamp_view_->SetText(relative_time);
timestamp_ = timestamp;
UpdateSummaryTextVisibility();
// Unretained is safe as the timer cancels the task on destruction.
timestamp_update_timer_.Start(
FROM_HERE, next_update,
base::BindOnce(&NotificationHeaderView::SetTimestamp,
base::Unretained(this), timestamp));
}
void NotificationHeaderView::SetDetailViewsVisible(bool visible) {
detail_views_->SetVisible(visible);
if (visible && timestamp_)
SetTimestamp(timestamp_.value());
else
timestamp_update_timer_.Stop();
UpdateSummaryTextVisibility();
}
void NotificationHeaderView::SetExpandButtonEnabled(bool enabled) {
expand_button_->SetVisible(enabled);
}
void NotificationHeaderView::SetExpanded(bool expanded) {
is_expanded_ = expanded;
expand_button_->SetImage(gfx::CreateVectorIcon(
expanded ? kNotificationExpandLessIcon : kNotificationExpandMoreIcon,
kExpandIconSize, accent_color_));
expand_button_->set_tooltip_text(l10n_util::GetStringUTF16(
expanded ? IDS_MESSAGE_CENTER_COLLAPSE_NOTIFICATION
: IDS_MESSAGE_CENTER_EXPAND_NOTIFICATION));
NotifyAccessibilityEvent(ax::mojom::Event::kStateChanged, true);
}
void NotificationHeaderView::SetAccentColor(SkColor color) {
accent_color_ = color;
app_name_view_->SetEnabledColor(accent_color_);
summary_text_view_->SetEnabledColor(accent_color_);
summary_text_divider_->SetEnabledColor(accent_color_);
SetExpanded(is_expanded_);
// If we are using the default app icon we should clear it so we refresh it
// with the new accent color.
if (using_default_app_icon_)
ClearAppIcon();
}
void NotificationHeaderView::SetBackgroundColor(SkColor color) {
app_name_view_->SetBackgroundColor(color);
summary_text_divider_->SetBackgroundColor(color);
summary_text_view_->SetBackgroundColor(color);
timestamp_divider_->SetBackgroundColor(color);
timestamp_view_->SetBackgroundColor(color);
}
void NotificationHeaderView::SetSubpixelRenderingEnabled(bool enabled) {
app_name_view_->SetSubpixelRenderingEnabled(enabled);
summary_text_divider_->SetSubpixelRenderingEnabled(enabled);
summary_text_view_->SetSubpixelRenderingEnabled(enabled);
timestamp_divider_->SetSubpixelRenderingEnabled(enabled);
timestamp_view_->SetSubpixelRenderingEnabled(enabled);
}
void NotificationHeaderView::HideAppIcon() {
app_icon_view_->SetVisible(false);
}
const base::string16& NotificationHeaderView::app_name_for_testing() const {
return app_name_view_->GetText();
}
const gfx::ImageSkia& NotificationHeaderView::app_icon_for_testing() const {
return app_icon_view_->GetImage();
}
const base::string16& NotificationHeaderView::timestamp_for_testing() const {
return timestamp_view_->GetText();
}
void NotificationHeaderView::UpdateSummaryTextVisibility() {
const bool summary_visible = !summary_text_view_->GetText().empty();
summary_text_divider_->SetVisible(summary_visible);
summary_text_view_->SetVisible(summary_visible);
const bool timestamp_visible = !has_progress_ && timestamp_;
timestamp_divider_->SetVisible(timestamp_visible);
timestamp_view_->SetVisible(timestamp_visible);
// TODO(crbug.com/991492): this should not be necessary.
detail_views_->InvalidateLayout();
}
} // namespace message_center