blob: 66ee5a39686c68da47646ff65b337f0d78a158c4 [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 "ash/system/message_center/notification_tray.h"
#include <memory>
#include "ash/accessibility/accessibility_controller.h"
#include "ash/message_center/message_center_bubble.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "ash/public/cpp/ash_switches.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/root_window_controller.h"
#include "ash/session/session_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_constants.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/message_center/ash_popup_alignment_delegate.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/tray/tray_bubble_wrapper.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/tray/tray_container.h"
#include "ash/system/tray/tray_utils.h"
#include "base/auto_reset.h"
#include "base/i18n/number_formatting.h"
#include "base/i18n/rtl.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/message_center_constants.h"
#include "ui/message_center/ui_controller.h"
#include "ui/message_center/views/message_popup_collection.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/bubble/tray_bubble_view.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/fill_layout.h"
namespace ash {
namespace {
constexpr int kMaximumSmallIconCount = 3;
constexpr int kTrayItemInnerIconSize = 16;
constexpr gfx::Size kTrayItemOuterSize(26, 26);
constexpr int kTrayMainAxisInset = 3;
constexpr int kTrayCrossAxisInset = 0;
constexpr int kTrayItemAnimationDurationMS = 200;
constexpr size_t kMaximumNotificationNumber = 99;
// in px. See https://crbug.com/754307.
constexpr size_t kPaddingFromScreenTop = 8;
constexpr float kBackgroundBlurRadius = 30.f;
// Flag to disable animation. Only for testing.
bool disable_animations_for_test = false;
} // namespace
// Class to initialize and manage the WebNotificationBubble and
// TrayBubbleWrapper instances for a bubble.
class NotificationBubbleWrapper {
public:
// Takes ownership of |bubble| and creates |bubble_wrapper_|.
NotificationBubbleWrapper(NotificationTray* tray,
TrayBackgroundView* anchor_tray,
MessageCenterBubble* bubble,
bool show_by_click) {
bubble_.reset(bubble);
views::TrayBubbleView::InitParams init_params;
init_params.delegate = tray;
init_params.parent_window = anchor_tray->GetBubbleWindowContainer();
init_params.anchor_view = anchor_tray->GetBubbleAnchor();
init_params.anchor_alignment = tray->GetAnchorAlignment();
const int width = message_center::kNotificationWidth +
message_center::kNotificationBorderThickness * 2 +
message_center::kMarginBetweenItemsInList * 2;
init_params.min_width = width;
init_params.max_width = width;
init_params.max_height = bubble->max_height();
init_params.show_by_click = show_by_click;
views::TrayBubbleView* bubble_view = new views::TrayBubbleView(init_params);
bubble_view->set_color(SK_ColorTRANSPARENT);
bubble_view->layer()->SetFillsBoundsOpaquely(false);
bubble_view->set_anchor_view_insets(anchor_tray->GetBubbleAnchorInsets());
bubble_wrapper_ = std::make_unique<TrayBubbleWrapper>(
tray, bubble_view, false /* is_persistent */);
bubble->InitializeContents(bubble_view);
if (app_list::features::IsBackgroundBlurEnabled()) {
// ClientView's layer (See TrayBubbleView::InitializeAndShowBubble())
bubble_view->layer()->parent()->SetBackgroundBlur(kBackgroundBlurRadius);
}
}
MessageCenterBubble* bubble() const { return bubble_.get(); }
// Convenience accessors.
views::TrayBubbleView* bubble_view() const { return bubble_->bubble_view(); }
private:
std::unique_ptr<MessageCenterBubble> bubble_;
std::unique_ptr<TrayBubbleWrapper> bubble_wrapper_;
DISALLOW_COPY_AND_ASSIGN(NotificationBubbleWrapper);
};
class WebNotificationItem : public views::View, public gfx::AnimationDelegate {
public:
WebNotificationItem(gfx::AnimationContainer* container,
NotificationTray* tray)
: tray_(tray) {
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
views::View::SetVisible(false);
set_owned_by_client();
SetLayoutManager(std::make_unique<views::FillLayout>());
animation_ = std::make_unique<gfx::SlideAnimation>(this);
animation_->SetContainer(container);
animation_->SetSlideDuration(kTrayItemAnimationDurationMS);
animation_->SetTweenType(gfx::Tween::LINEAR);
}
void SetVisible(bool set_visible) override {
if (!GetWidget() || disable_animations_for_test) {
views::View::SetVisible(set_visible);
return;
}
if (!set_visible) {
animation_->Hide();
AnimationProgressed(animation_.get());
} else {
animation_->Show();
AnimationProgressed(animation_.get());
views::View::SetVisible(true);
}
}
void HideAndDelete() {
SetVisible(false);
if (!visible() && !animation_->is_animating()) {
if (parent())
parent()->RemoveChildView(this);
base::ThreadTaskRunnerHandle::Get()->DeleteSoon(FROM_HERE, this);
} else {
delete_after_animation_ = true;
}
}
protected:
// Overridden from views::View:
gfx::Size CalculatePreferredSize() const override {
if (!animation_.get() || !animation_->is_animating())
return kTrayItemOuterSize;
// Animate the width (or height) when this item shows (or hides) so that
// the icons on the left are shifted with the animation.
// Note that TrayItemView does the same thing.
gfx::Size size = kTrayItemOuterSize;
if (tray_->shelf()->IsHorizontalAlignment()) {
size.set_width(std::max(
1, gfx::ToRoundedInt(size.width() * animation_->GetCurrentValue())));
} else {
size.set_height(std::max(
1, gfx::ToRoundedInt(size.height() * animation_->GetCurrentValue())));
}
return size;
}
int GetHeightForWidth(int width) const override {
return GetPreferredSize().height();
}
private:
// gfx::AnimationDelegate:
void AnimationProgressed(const gfx::Animation* animation) override {
gfx::Transform transform;
if (tray_->shelf()->IsHorizontalAlignment()) {
transform.Translate(0, animation->CurrentValueBetween(
static_cast<double>(height()) / 2., 0.));
} else {
transform.Translate(
animation->CurrentValueBetween(static_cast<double>(width() / 2.), 0.),
0);
}
transform.Scale(animation->GetCurrentValue(), animation->GetCurrentValue());
layer()->SetTransform(transform);
PreferredSizeChanged();
}
void AnimationEnded(const gfx::Animation* animation) override {
if (animation->GetCurrentValue() < 0.1)
views::View::SetVisible(false);
if (delete_after_animation_) {
if (parent())
parent()->RemoveChildView(this);
base::ThreadTaskRunnerHandle::Get()->DeleteSoon(FROM_HERE, this);
}
}
void AnimationCanceled(const gfx::Animation* animation) override {
AnimationEnded(animation);
}
std::unique_ptr<gfx::SlideAnimation> animation_;
bool delete_after_animation_ = false;
NotificationTray* tray_;
DISALLOW_COPY_AND_ASSIGN(WebNotificationItem);
};
class NotificationImage : public WebNotificationItem {
public:
NotificationImage(const gfx::ImageSkia& image,
gfx::AnimationContainer* container,
NotificationTray* tray)
: WebNotificationItem(container, tray) {
DCHECK(image.size() ==
gfx::Size(kTrayItemInnerIconSize, kTrayItemInnerIconSize));
view_ = new views::ImageView();
view_->SetImage(image);
view_->SetTooltipText(
l10n_util::GetStringUTF16(IDS_ASH_MESSAGE_CENTER_FOOTER_TITLE));
AddChildView(view_);
}
private:
views::ImageView* view_;
DISALLOW_COPY_AND_ASSIGN(NotificationImage);
};
class NotificationLabel : public WebNotificationItem {
public:
NotificationLabel(gfx::AnimationContainer* container, NotificationTray* tray)
: WebNotificationItem(container, tray) {
view_ = new views::Label();
SetupLabelForTray(view_);
AddChildView(view_);
}
void SetNotificationCount(bool small_icons_exist, size_t notification_count) {
notification_count = std::min(notification_count,
kMaximumNotificationNumber); // cap with 99
// TODO(yoshiki): Use a string for "99" and "+99".
base::string16 str = base::FormatNumber(notification_count);
if (small_icons_exist) {
str = base::ASCIIToUTF16("+") + str;
if (base::i18n::IsRTL())
base::i18n::WrapStringWithRTLFormatting(&str);
}
view_->SetText(str);
SchedulePaint();
}
private:
views::Label* view_;
DISALLOW_COPY_AND_ASSIGN(NotificationLabel);
};
NotificationTray::NotificationTray(Shelf* shelf,
aura::Window* status_area_window)
: TrayBackgroundView(shelf),
status_area_window_(status_area_window),
show_message_center_on_unlock_(false),
should_update_tray_content_(false) {
DCHECK(shelf);
DCHECK(status_area_window_);
SetInkDropMode(InkDropMode::ON);
gfx::ImageSkia bell_image =
CreateVectorIcon(kShelfNotificationsIcon, kShelfIconColor);
bell_icon_ = std::make_unique<NotificationImage>(
bell_image, animation_container_.get(), this);
tray_container()->AddChildView(bell_icon_.get());
gfx::ImageSkia quiet_mode_image =
CreateVectorIcon(kNotificationCenterDoNotDisturbOnIcon,
kTrayItemInnerIconSize, kShelfIconColor);
quiet_mode_icon_ = std::make_unique<NotificationImage>(
quiet_mode_image, animation_container_.get(), this);
tray_container()->AddChildView(quiet_mode_icon_.get());
counter_ =
std::make_unique<NotificationLabel>(animation_container_.get(), this);
tray_container()->AddChildView(counter_.get());
message_center_ui_controller_ =
std::make_unique<message_center::UiController>(this);
popup_alignment_delegate_ =
std::make_unique<AshPopupAlignmentDelegate>(shelf);
popup_collection_ = std::make_unique<message_center::MessagePopupCollection>(
message_center(), message_center_ui_controller_.get(),
popup_alignment_delegate_.get());
display::Screen* screen = display::Screen::GetScreen();
popup_alignment_delegate_->StartObserving(
screen, screen->GetDisplayNearestWindow(status_area_window_));
OnMessageCenterContentsChanged();
tray_container()->SetMargin(kTrayMainAxisInset, kTrayCrossAxisInset);
}
NotificationTray::~NotificationTray() {
// Release any child views that might have back pointers before ~View().
message_center_bubble_.reset();
popup_alignment_delegate_.reset();
popup_collection_.reset();
}
// static
void NotificationTray::DisableAnimationsForTest(bool disable) {
disable_animations_for_test = disable;
}
// Public methods.
bool NotificationTray::ShowMessageCenterInternal(bool show_settings,
bool show_by_click) {
if (!ShouldShowMessageCenter())
return false;
if (IsMessageCenterVisible())
return true;
if (switches::IsSidebarEnabled()) {
SidebarInitMode mode =
(!show_settings ? SidebarInitMode::NORMAL
: SidebarInitMode::MESSAGE_CENTER_SETTINGS);
// TODO(yoshiki): Support non-primary desktop on multi-display environment.
Shell::Get()->GetPrimaryRootWindowController()->sidebar()->Show(mode);
} else {
MessageCenterBubble* message_center_bubble =
new MessageCenterBubble(message_center());
// In the horizontal case, message center starts from the top of the shelf.
// In the vertical case, it starts from the bottom of NotificationTray.
const int max_height = (shelf()->IsHorizontalAlignment()
? shelf()->GetUserWorkAreaBounds().height()
: GetBoundsInScreen().bottom() -
shelf()->GetUserWorkAreaBounds().y());
// Sets the maximum height, considering the padding from the top edge of
// screen. This padding should be applied in all types of shelf alignment.
message_center_bubble->SetMaxHeight(max_height - kPaddingFromScreenTop);
if (show_settings)
message_center_bubble->SetSettingsVisible();
// For vertical shelf alignments, anchor to the NotificationTray, but for
// horizontal (i.e. bottom) shelves, anchor to the system tray.
TrayBackgroundView* anchor_tray = this;
if (shelf()->IsHorizontalAlignment())
anchor_tray = shelf()->GetSystemTrayAnchor();
message_center_bubble_ = std::make_unique<NotificationBubbleWrapper>(
this, anchor_tray, message_center_bubble, show_by_click);
}
shelf()->UpdateAutoHideState();
SetIsActive(true);
return true;
}
bool NotificationTray::ShowMessageCenter(bool show_by_click) {
return ShowMessageCenterInternal(false /* show_settings */, show_by_click);
}
void NotificationTray::HideMessageCenter() {
if ((switches::IsSidebarEnabled() && !IsMessageCenterVisible()) ||
(!switches::IsSidebarEnabled() && !message_center_bubble()))
return;
SetIsActive(false);
if (switches::IsSidebarEnabled()) {
Sidebar* sidebar =
RootWindowController::ForWindow(GetWidget()->GetNativeView())
->sidebar();
if (sidebar)
sidebar->Hide();
} else {
message_center_bubble_.reset();
}
show_message_center_on_unlock_ = false;
shelf()->UpdateAutoHideState();
}
void NotificationTray::SetTrayBubbleHeight(int height) {
popup_alignment_delegate_->SetTrayBubbleHeight(height);
}
int NotificationTray::tray_bubble_height_for_test() const {
return popup_alignment_delegate_->tray_bubble_height_for_test();
}
bool NotificationTray::ShowPopups() {
if (IsMessageCenterVisible())
return false;
popup_collection_->DoUpdate();
return true;
}
void NotificationTray::HidePopups() {
DCHECK(popup_collection_.get());
popup_collection_->MarkAllPopupsShown();
}
// Private methods.
bool NotificationTray::ShouldShowMessageCenter() const {
// Hidden at login screen, during supervised user creation, etc.
return Shell::Get()->session_controller()->ShouldShowNotificationTray();
}
bool NotificationTray::IsMessageCenterVisible() const {
if (switches::IsSidebarEnabled()) {
Sidebar* sidebar =
RootWindowController::ForWindow(GetWidget()->GetNativeView())
->sidebar();
return sidebar && sidebar->IsVisible();
} else {
return message_center_bubble() &&
message_center_bubble()->bubble()->IsVisible();
}
}
void NotificationTray::UpdateAfterShelfAlignmentChange() {
TrayBackgroundView::UpdateAfterShelfAlignmentChange();
// Destroy existing message center bubble so that it won't be reused.
message_center_ui_controller_->HideMessageCenterBubble();
// Destroy any existing popup bubbles and rebuilt if necessary.
message_center_ui_controller_->HidePopupBubble();
message_center_ui_controller_->ShowPopupBubble();
}
void NotificationTray::UpdateAfterRootWindowBoundsChange(
const gfx::Rect& old_bounds,
const gfx::Rect& new_bounds) {
TrayBackgroundView::UpdateAfterRootWindowBoundsChange(old_bounds, new_bounds);
// Hide the message center bubble, since the bounds may not have enough to
// show the current size of the message center. This handler is invoked when
// the screen is rotated or the screen size is changed.
message_center_ui_controller_->HideMessageCenterBubble();
}
void NotificationTray::AnchorUpdated() {
if (message_center_bubble()) {
UpdateClippingWindowBounds();
shelf()->GetSystemTrayAnchor()->UpdateClippingWindowBounds();
message_center_bubble()->bubble_view()->UpdateBubble();
// Should check |message_center_bubble_| again here. Since UpdateBubble
// above set the bounds of the bubble which will stop the current
// animation. If web notification bubble is during animation to close,
// CloseBubbleObserver in TrayBackgroundView will close the bubble if
// animation finished.
if (message_center_bubble())
UpdateBubbleViewArrow(message_center_bubble()->bubble_view());
}
}
base::string16 NotificationTray::GetAccessibleNameForTray() {
return l10n_util::GetStringFUTF16Int(
IDS_MESSAGE_CENTER_ACCESSIBLE_NAME,
static_cast<int>(message_center_ui_controller_->message_center()
->NotificationCount()));
}
void NotificationTray::HideBubbleWithView(
const views::TrayBubbleView* bubble_view) {
if (message_center_bubble() &&
bubble_view == message_center_bubble()->bubble_view()) {
message_center_ui_controller_->HideMessageCenterBubble();
} else if (popup_collection_.get()) {
message_center_ui_controller_->HidePopupBubble();
}
}
void NotificationTray::BubbleViewDestroyed() {
if (message_center_bubble())
message_center_bubble()->bubble()->BubbleViewDestroyed();
}
void NotificationTray::OnMouseEnteredView() {}
void NotificationTray::OnMouseExitedView() {}
base::string16 NotificationTray::GetAccessibleNameForBubble() {
return GetAccessibleNameForTray();
}
bool NotificationTray::ShouldEnableExtraKeyboardAccessibility() {
return Shell::Get()->accessibility_controller()->IsSpokenFeedbackEnabled();
}
void NotificationTray::HideBubble(const views::TrayBubbleView* bubble_view) {
HideBubbleWithView(bubble_view);
}
bool NotificationTray::ShowNotifierSettings() {
if (IsMessageCenterVisible()) {
if (switches::IsSidebarEnabled()) {
Sidebar* sidebar =
RootWindowController::ForWindow(GetWidget()->GetNativeView())
->sidebar();
if (sidebar)
sidebar->Show(SidebarInitMode::MESSAGE_CENTER_SETTINGS);
} else {
static_cast<MessageCenterBubble*>(message_center_bubble()->bubble())
->SetSettingsVisible();
}
return true;
}
return ShowMessageCenterInternal(true /* show_settings */,
false /* show_by_click */);
}
void NotificationTray::OnMessageCenterContentsChanged() {
// Do not update the tray contents directly. Multiple change events can happen
// consecutively, and calling Update in the middle of those events will show
// intermediate unread counts for a moment.
should_update_tray_content_ = true;
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(&NotificationTray::UpdateTrayContent, AsWeakPtr()));
}
void NotificationTray::UpdateTrayContent() {
if (!should_update_tray_content_)
return;
should_update_tray_content_ = false;
std::unordered_set<std::string> notification_ids;
for (auto& pair : visible_small_icons_)
notification_ids.insert(pair.first);
// Add small icons (up to kMaximumSmallIconCount = 3).
message_center::MessageCenter* message_center =
message_center_ui_controller_->message_center();
size_t visible_small_icon_count = 0;
for (const auto* notification : message_center->GetVisibleNotifications()) {
gfx::Image image = notification->GenerateMaskedSmallIcon(
kTrayItemInnerIconSize, kTrayIconColor);
if (image.IsEmpty())
continue;
if (visible_small_icon_count >= kMaximumSmallIconCount)
break;
visible_small_icon_count++;
notification_ids.erase(notification->id());
if (visible_small_icons_.count(notification->id()) != 0)
continue;
auto item = std::make_unique<NotificationImage>(
image.AsImageSkia(), animation_container_.get(), this);
tray_container()->AddChildViewAt(item.get(), 0);
item->SetVisible(true);
visible_small_icons_.insert(
std::make_pair(notification->id(), std::move(item)));
}
// Remove unnecessary icons.
for (const std::string& id : notification_ids) {
NotificationImage* item = visible_small_icons_[id].release();
visible_small_icons_.erase(id);
item->HideAndDelete();
}
// Show or hide the bell icon.
size_t visible_notification_count = message_center->NotificationCount();
bell_icon_->SetVisible(visible_notification_count == 0 &&
!message_center->IsQuietMode());
quiet_mode_icon_->SetVisible(visible_notification_count == 0 &&
message_center->IsQuietMode());
// Show or hide the counter.
size_t hidden_icon_count =
visible_notification_count - visible_small_icon_count;
if (hidden_icon_count != 0) {
counter_->SetVisible(true);
counter_->SetNotificationCount(
(visible_small_icon_count != 0), // small_icons_exist
hidden_icon_count);
} else {
counter_->SetVisible(false);
}
SetVisible(ShouldShowMessageCenter());
PreferredSizeChanged();
Layout();
SchedulePaint();
}
void NotificationTray::ClickedOutsideBubble() {
// Only hide the message center
if (!IsMessageCenterVisible())
return;
message_center_ui_controller_->HideMessageCenterBubble();
}
bool NotificationTray::PerformAction(const ui::Event& event) {
UserMetricsRecorder::RecordUserClickOnTray(
LoginMetricsRecorder::TrayClickTarget::kNotificationTray);
if (IsMessageCenterVisible())
CloseBubble();
else
ShowBubble(event.IsMouseEvent() || event.IsGestureEvent());
return true;
}
void NotificationTray::CloseBubble() {
message_center_ui_controller_->HideMessageCenterBubble();
}
void NotificationTray::ShowBubble(bool show_by_click) {
if (!IsMessageCenterVisible())
message_center_ui_controller_->ShowMessageCenterBubble(show_by_click);
}
views::TrayBubbleView* NotificationTray::GetBubbleView() {
return message_center_bubble_ ? message_center_bubble_->bubble_view()
: nullptr;
}
message_center::MessageCenter* NotificationTray::message_center() const {
return message_center_ui_controller_->message_center();
}
// Methods for testing
bool NotificationTray::IsPopupVisible() const {
return message_center_ui_controller_->popups_visible();
}
MessageCenterBubble* NotificationTray::GetMessageCenterBubbleForTest() {
if (!message_center_bubble())
return nullptr;
return static_cast<MessageCenterBubble*>(message_center_bubble()->bubble());
}
} // namespace ash