blob: 65eb25ab23b96892db716fcd4962f85430e7c3be [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// 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/message_view.h"
#include "base/observer_list.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.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/base/metadata/metadata_impl_macros.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/shadow_util.h"
#include "ui/gfx/shadow_value.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/message_center_constants.h"
#include "ui/message_center/views/notification_background_painter.h"
#include "ui/message_center/views/notification_control_buttons_view.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/widget/widget.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "base/time/time.h"
#endif
namespace message_center {
namespace {
bool ShouldShowAeroShadowBorder() {
#if BUILDFLAG(IS_WIN)
return true;
#else
return false;
#endif
}
// Helper function to setup focus ring shapes for `MessageView`.
void InstallHighlightPathGenerator(views::View* view,
float top_radius,
float bottom_radius) {
auto corners = gfx::RoundedCornersF{top_radius, top_radius, bottom_radius,
bottom_radius};
// Shrink focus ring size by -`kDefaultHaloInset` on each side to draw
// them on top of the notifications. We need to do this because
// `TrayBubbleView` and `MessagePopupView` has a layer that masks to bounds
// due to which the focus ring can not extend outside the view.
views::HighlightPathGenerator::Install(
view, std::make_unique<views::RoundRectHighlightPathGenerator>(
gfx::Insets(-views::FocusRing::kDefaultHaloInset), corners));
}
} // namespace
MessageView::MessageView(const Notification& notification)
: notification_id_(notification.id()),
notifier_id_(notification.notifier_id()),
timestamp_(notification.timestamp()),
slide_out_controller_(this, this) {
SetNotifyEnterExitOnChild(true);
slide_out_controller_.set_trackpad_gestures_enabled(true);
SetFocusBehavior(FocusBehavior::ALWAYS);
views::FocusRing::Install(this);
views::FocusRing::Get(this)->SetOutsetFocusRingDisabled(true);
// Paint to a dedicated layer to make the layer non-opaque.
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
GetViewAccessibility().SetRole(ax::mojom::Role::kGenericContainer);
GetViewAccessibility().SetRoleDescription(
l10n_util::GetStringUTF8(IDS_MESSAGE_NOTIFICATION_ACCESSIBLE_NAME));
UpdateWithNotification(notification);
UpdateCornerRadius(0, 0);
// If Aero is enabled, set shadow border.
if (ShouldShowAeroShadowBorder()) {
const auto& shadow = gfx::ShadowDetails::Get(2, 0);
gfx::Insets ninebox_insets = gfx::ShadowValue::GetBlurRegion(shadow.values);
SetBorder(views::CreateBorderPainter(
views::Painter::CreateImagePainter(shadow.nine_patch_image,
ninebox_insets),
-gfx::ShadowValue::GetMargin(shadow.values)));
}
}
MessageView::~MessageView() {
RemovedFromWidget();
}
views::View* MessageView::FindGroupNotificationView(
const std::string& notification_id) {
// Not implemented by default.
return nullptr;
}
// Creates text for spoken feedback from the data contained in the
// notification.
std::u16string MessageView::CreateAccessibleName(
const Notification& notification) {
if (!notification.accessible_name().empty())
return notification.accessible_name();
// Fall back to text constructed from the notification.
// Add non-empty elements.
std::vector<std::u16string> accessible_lines;
if (!notification.title().empty())
accessible_lines.push_back(notification.title());
if (!notification.message().empty())
accessible_lines.push_back(notification.message());
if (!notification.context_message().empty())
accessible_lines.push_back(notification.context_message());
std::vector<NotificationItem> items = notification.items();
for (size_t i = 0; i < items.size() && i < kNotificationMaximumItems; ++i) {
accessible_lines.push_back(items[i].title() + u" " + items[i].message());
}
return base::JoinString(accessible_lines, u"\n");
}
void MessageView::UpdateWithNotification(const Notification& notification) {
pinned_ = notification.pinned();
std::u16string name = CreateAccessibleName(notification);
if (name.empty()) {
GetViewAccessibility().SetName(
std::u16string(), ax::mojom::NameFrom::kAttributeExplicitlyEmpty);
} else {
GetViewAccessibility().SetName(name);
}
if (updated_name_callback_) {
updated_name_callback_.Run(
notification.rich_notification_data()
.should_make_spoken_feedback_for_popup_updates);
}
slide_out_controller_.set_slide_mode(CalculateSlideMode());
}
void MessageView::SetIsNested() {
DCHECK(!is_nested_) << "MessageView::SetIsNested() is called twice wrongly.";
is_nested_ = true;
// Update enability since it might be changed by "is_nested" flag.
slide_out_controller_.set_slide_mode(CalculateSlideMode());
slide_out_controller_.set_update_opacity(false);
UpdateNestedBorder();
if (GetControlButtonsView())
GetControlButtonsView()->ShowCloseButton(GetMode() != Mode::PINNED);
}
void MessageView::CloseSwipeControl() {
slide_out_controller_.CloseSwipeControl();
}
void MessageView::SlideOutAndClose(int direction) {
// Do not process events once the message view is animating out.
// crbug.com/940719
SetEnabled(false);
slide_out_controller_.SlideOutAndClose(direction);
}
void MessageView::SetExpanded(bool expanded) {
MessageCenter::Get()->OnSetExpanded(notification_id_, expanded);
}
bool MessageView::IsExpanded() const {
// Not implemented by default.
return false;
}
bool MessageView::IsAutoExpandingAllowed() const {
// Allowed by default.
return true;
}
bool MessageView::IsManuallyExpandedOrCollapsed() const {
// Not implemented by default.
return false;
}
void MessageView::SetManuallyExpandedOrCollapsed(ExpandState state) {
// Not implemented by default.
}
void MessageView::ToggleInlineSettings(const ui::Event& event) {
// Not implemented by default.
}
void MessageView::ToggleSnoozeSettings(const ui::Event& event) {
// Not implemented by default.
}
void MessageView::UpdateCornerRadius(int top_radius, int bottom_radius) {
SetCornerRadius(top_radius, bottom_radius);
if (!GetWidget()) {
return;
}
UpdateBackgroundPainter();
SchedulePaint();
}
void MessageView::OnContainerAnimationStarted() {
// Not implemented by default.
}
void MessageView::OnContainerAnimationEnded() {
// Not implemented by default.
}
bool MessageView::OnMousePressed(const ui::MouseEvent& event) {
return true;
}
bool MessageView::OnMouseDragged(const ui::MouseEvent& event) {
return true;
}
void MessageView::OnMouseReleased(const ui::MouseEvent& event) {
if (!event.IsOnlyLeftMouseButton()) {
return;
}
MessageCenter::Get()->ClickOnNotification(notification_id_);
}
void MessageView::OnMouseEntered(const ui::MouseEvent& event) {
UpdateControlButtonsVisibility();
MessageCenter::Get()->OnMessageViewHovered(notification_id_);
}
void MessageView::OnMouseExited(const ui::MouseEvent& event) {
UpdateControlButtonsVisibility();
}
bool MessageView::OnKeyPressed(const ui::KeyEvent& event) {
if (event.flags() != ui::EF_NONE) {
return false;
}
if (event.key_code() == ui::VKEY_RETURN) {
MessageCenter::Get()->ClickOnNotification(notification_id_);
return true;
} else if ((event.key_code() == ui::VKEY_DELETE ||
event.key_code() == ui::VKEY_BACK)) {
MessageCenter::Get()->RemoveNotification(notification_id_,
true /* by_user */);
return true;
}
return false;
}
bool MessageView::OnKeyReleased(const ui::KeyEvent& event) {
// Space key handling is triggered at key-release timing. See
// ui/views/controls/buttons/button.cc for why.
if (event.flags() != ui::EF_NONE || event.key_code() != ui::VKEY_SPACE)
return false;
MessageCenter::Get()->ClickOnNotification(notification_id_);
return true;
}
void MessageView::OnPaint(gfx::Canvas* canvas) {
if (ShouldShowAeroShadowBorder()) {
// If the border is shadow, paint border first.
OnPaintBorder(canvas);
// Clip at the border so we don't paint over it.
canvas->ClipRect(GetContentsBounds());
OnPaintBackground(canvas);
} else {
views::View::OnPaint(canvas);
}
}
void MessageView::OnBlur() {
views::View::OnBlur();
// We paint a focus indicator.
SchedulePaint();
}
void MessageView::OnGestureEvent(ui::GestureEvent* event) {
if (event->type() == ui::EventType::kGestureTap) {
MessageCenter::Get()->ClickOnNotification(notification_id_);
event->SetHandled();
return;
}
if (!event->IsScrollGestureEvent() && !event->IsFlingScrollEvent()) {
return;
}
if (scroller_) {
scroller_->OnGestureEvent(event);
}
event->SetHandled();
}
void MessageView::RemovedFromWidget() {
if (!focus_manager_) {
return;
}
focus_manager_->RemoveFocusChangeListener(this);
focus_manager_ = nullptr;
}
void MessageView::AddedToWidget() {
focus_manager_ = GetFocusManager();
if (focus_manager_) {
focus_manager_->AddFocusChangeListener(this);
}
}
void MessageView::OnThemeChanged() {
View::OnThemeChanged();
UpdateNestedBorder();
UpdateBackgroundPainter();
}
ui::Layer* MessageView::GetSlideOutLayer() {
// If a message view is contained in a parent message view it should give up
// slide behavior to the parent message view when the parent view is
// collapsed.
auto* nested_layer = (ShouldParentHandleSlide() && parent_message_view_)
? parent_message_view_->layer()
: layer();
bool is_nested = (ShouldParentHandleSlide() && parent_message_view_)
? parent_message_view_->is_nested()
: is_nested_;
return is_nested ? nested_layer : GetWidget()->GetLayer();
}
void MessageView::OnSlideStarted() {
observers_.Notify(&Observer::OnSlideStarted, notification_id_);
}
void MessageView::OnSlideChanged(bool in_progress) {
// crbug/1333664: We need to make sure to disable scrolling while a
// notification view is sliding. This is to ensure the notification view can
// only move horizontally or vertically at one time.
if (scroller_ && !is_sliding_ && slide_out_controller_.GetGestureAmount()) {
is_sliding_ = true;
scroller_->SetVerticalScrollBarMode(
views::ScrollView::ScrollBarMode::kDisabled);
}
if (scroller_ && !in_progress) {
is_sliding_ = false;
scroller_->SetVerticalScrollBarMode(
views::ScrollView::ScrollBarMode::kEnabled);
}
if (in_progress) {
observers_.Notify(&Observer::OnSlideChanged, notification_id_);
} else {
observers_.Notify(&Observer::OnSlideEnded, notification_id_);
}
}
void MessageView::AddObserver(MessageView::Observer* observer) {
observers_.AddObserver(observer);
}
void MessageView::RemoveObserver(MessageView::Observer* observer) {
observers_.RemoveObserver(observer);
}
void MessageView::OnSlideOut() {
if (ShouldParentHandleSlide() && parent_message_view_)
return parent_message_view_->OnSlideOut();
// The notification may be deleted after slide out, so give observers a
// chance to handle the notification before fulling sliding out.
observers_.Notify(&Observer::OnPreSlideOut, notification_id_);
// Copy the |notification_id| here as calling OnSlideOut() might destroy
// |this| but we still want to call RemoveNotification(). Note that the
// iteration over |observers_| is still safe and will simply stop.
std::string notification_id_copy = notification_id_;
observers_.Notify(&Observer::OnSlideOut, notification_id_);
auto* message_center = MessageCenter::Get();
if (message_center->FindPopupNotificationById(notification_id_copy)) {
message_center->MarkSinglePopupAsShown(notification_id_copy,
/*mark_notification_as_read=*/true);
return;
}
message_center->RemoveNotification(notification_id_copy, /*by_user=*/true);
}
void MessageView::OnDidChangeFocus(views::View* before, views::View* now) {
if (Contains(before) || Contains(now) ||
(GetControlButtonsView() && (GetControlButtonsView()->Contains(before) ||
GetControlButtonsView()->Contains(now)))) {
UpdateControlButtonsVisibility();
}
}
views::SlideOutController::SlideMode MessageView::CalculateSlideMode() const {
if (disable_slide_) {
return views::SlideOutController::SlideMode::kNone;
}
switch (GetMode()) {
case Mode::SETTING:
return views::SlideOutController::SlideMode::kNone;
case Mode::PINNED:
return views::SlideOutController::SlideMode::kPartial;
case Mode::NORMAL:
return views::SlideOutController::SlideMode::kFull;
}
NOTREACHED();
}
MessageView::Mode MessageView::GetMode() const {
if (setting_mode_) {
return Mode::SETTING;
}
// Only nested notifications can be pinned. Standalones (i.e. popups) can't
// be.
if (pinned_ && is_nested_) {
return Mode::PINNED;
}
return Mode::NORMAL;
}
float MessageView::GetSlideAmount() const {
if (slide_out_controller_.mode() ==
views::SlideOutController::SlideMode::kNone) {
// The return value of this method is used by NotificationSwipeControlView
// to determine visibility of the setting button. Returning 0 not to show
// the setting button with SlideMode::kNone.
return 0.f;
}
return slide_out_controller_.gesture_amount();
}
void MessageView::SetSettingMode(bool setting_mode) {
setting_mode_ = setting_mode;
slide_out_controller_.set_slide_mode(CalculateSlideMode());
UpdateControlButtonsVisibility();
}
void MessageView::DisableNotification() {
MessageCenter::Get()->DisableNotification(notification_id());
}
void MessageView::DisableSlideForcibly(bool disable) {
disable_slide_ = disable;
slide_out_controller_.set_slide_mode(CalculateSlideMode());
}
void MessageView::SetSlideButtonWidth(int control_button_width) {
slide_out_controller_.SetSwipeControlWidth(control_button_width);
}
void MessageView::SetCornerRadius(int top_radius, int bottom_radius) {
top_radius_ = top_radius;
bottom_radius_ = bottom_radius;
InstallHighlightPathGenerator(this, top_radius, bottom_radius);
}
void MessageView::OnCloseButtonPressed() {
observers_.Notify(&Observer::OnCloseButtonPressed, notification_id_);
MessageCenter::Get()->RemoveNotification(notification_id_,
true /* by_user */);
}
void MessageView::OnSettingsButtonPressed(const ui::Event& event) {
observers_.Notify(&Observer::OnSettingsButtonPressed, notification_id_);
if (inline_settings_enabled_) {
ToggleInlineSettings(event);
} else {
MessageCenter::Get()->ClickOnSettingsButton(notification_id_);
}
}
void MessageView::OnSnoozeButtonPressed(const ui::Event& event) {
observers_.Notify(&Observer::OnSnoozeButtonPressed, notification_id_);
if (snooze_settings_enabled_) {
ToggleSnoozeSettings(event);
} else {
MessageCenter::Get()->ClickOnSnoozeButton(notification_id());
}
}
#if BUILDFLAG(IS_CHROMEOS)
base::TimeDelta MessageView::GetBoundsAnimationDuration(
const Notification& notification) const {
return base::Milliseconds(0);
}
#endif
bool MessageView::ShouldShowControlButtons() const {
#if BUILDFLAG(IS_CHROMEOS)
// Users on ChromeOS are used to the Settings and Close buttons not being
// visible at all times, but users on other platforms expect them to be
// visible.
auto* control_buttons_view = GetControlButtonsView();
return control_buttons_view &&
(control_buttons_view->IsAnyButtonFocused() ||
(GetMode() != Mode::SETTING && IsMouseHovered()) ||
MessageCenter::Get()->IsSpokenFeedbackEnabled());
#else
return true;
#endif
}
bool MessageView::ShouldParentHandleSlide() const {
if (!parent_message_view_) {
return false;
}
return !parent_message_view_->IsExpanded();
}
void MessageView::UpdateBackgroundPainter() {
const auto* color_provider = GetColorProvider();
SkColor background_color =
color_provider->GetColor(ui::kColorNotificationBackgroundActive);
SetBackground(views::CreateBackgroundFromPainter(
std::make_unique<NotificationBackgroundPainter>(
top_radius_, bottom_radius_, background_color)));
}
void MessageView::UpdateNestedBorder() {
if (!is_nested_ || !GetWidget()) {
return;
}
SkColor border_color;
#if BUILDFLAG(IS_CHROMEOS)
border_color = SK_ColorTRANSPARENT;
#else
border_color =
GetColorProvider()->GetColor(ui::kColorFocusableBorderUnfocused);
#endif
SetBorder(views::CreateRoundedRectBorder(
kNotificationBorderThickness, kNotificationCornerRadius, border_color));
}
void MessageView::UpdateControlButtonsVisibility() {
auto* control_buttons_view = GetControlButtonsView();
if (control_buttons_view)
control_buttons_view->ShowButtons(ShouldShowControlButtons());
}
void MessageView::UpdateControlButtonsVisibilityWithNotification(
const Notification& notification) {
auto* control_buttons_view = GetControlButtonsView();
if (control_buttons_view) {
control_buttons_view->ShowButtons(ShouldShowControlButtons());
control_buttons_view->ShowSettingsButton(
notification.should_show_settings_button());
control_buttons_view->ShowSnoozeButton(
notification.should_show_snooze_button());
control_buttons_view->ShowCloseButton(GetMode() != Mode::PINNED);
}
UpdateControlButtonsVisibility();
}
void MessageView::SetUpdatedNameCallback(UpdatedNameCallback callback) {
CHECK(callback);
updated_name_callback_ = std::move(callback);
}
BEGIN_METADATA(MessageView)
END_METADATA
} // namespace message_center