blob: b6bd17a5cb56864ff8f7bc33ea915660f3499c08 [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/common/system/web_notification/web_notification_tray.h"
#include "ash/common/material_design/material_design_controller.h"
#include "ash/common/session/session_state_delegate.h"
#include "ash/common/shelf/shelf_constants.h"
#include "ash/common/shelf/wm_shelf.h"
#include "ash/common/shelf/wm_shelf_util.h"
#include "ash/common/system/tray/system_tray.h"
#include "ash/common/system/tray/system_tray_delegate.h"
#include "ash/common/system/tray/tray_bubble_wrapper.h"
#include "ash/common/system/tray/tray_constants.h"
#include "ash/common/system/tray/tray_utils.h"
#include "ash/common/system/web_notification/ash_popup_alignment_delegate.h"
#include "ash/common/wm_lookup.h"
#include "ash/common/wm_root_window_controller.h"
#include "ash/common/wm_shell.h"
#include "ash/common/wm_window.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/resources/vector_icons/vector_icons.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 "grit/ash_strings.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/gfx/vector_icons_public.h"
#include "ui/message_center/message_center_style.h"
#include "ui/message_center/message_center_tray_delegate.h"
#include "ui/message_center/views/message_bubble_base.h"
#include "ui/message_center/views/message_center_bubble.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/controls/menu/menu_runner.h"
#include "ui/views/layout/fill_layout.h"
#if defined(OS_CHROMEOS)
namespace message_center {
MessageCenterTrayDelegate* CreateMessageCenterTray() {
// On Windows+Ash the Tray will not be hosted in ash::Shell.
NOTREACHED();
return nullptr;
}
} // namespace message_center
#endif // defined(OS_CHROMEOS)
namespace ash {
namespace {
// Menu commands
constexpr int kToggleQuietMode = 0;
constexpr int kEnableQuietModeDay = 2;
constexpr int kMaximumSmallIconCount = 3;
constexpr gfx::Size kTrayItemInnerIconSize(16, 16);
constexpr gfx::Size kTrayItemInnerBellIconSizeNonMd(18, 18);
constexpr gfx::Size kTrayItemOuterSize(26, 26);
constexpr int kTrayMainAxisInset = 3;
constexpr int kTrayCrossAxisInset = 0;
constexpr int kTrayItemAnimationDurationMS = 200;
constexpr size_t kMaximumNotificationNumber = 99;
// Flag to disable animation. Only for testing.
bool disable_animations_for_test = false;
}
namespace {
const SkColor kWebNotificationColorNoUnread =
SkColorSetARGB(128, 255, 255, 255);
const SkColor kWebNotificationColorWithUnread = SK_ColorWHITE;
const int kNoUnreadIconSize = 18;
} // namespace
// Class to initialize and manage the WebNotificationBubble and
// TrayBubbleWrapper instances for a bubble.
class WebNotificationBubbleWrapper {
public:
// Takes ownership of |bubble| and creates |bubble_wrapper_|.
WebNotificationBubbleWrapper(WebNotificationTray* tray,
message_center::MessageBubbleBase* bubble) {
bubble_.reset(bubble);
views::TrayBubbleView::AnchorAlignment anchor_alignment =
tray->GetAnchorAlignment();
views::TrayBubbleView::InitParams init_params =
bubble->GetInitParams(anchor_alignment);
views::TrayBubbleView* bubble_view = views::TrayBubbleView::Create(
tray->GetBubbleAnchor(), tray, &init_params);
bubble_view->set_anchor_view_insets(tray->GetBubbleAnchorInsets());
bubble_wrapper_.reset(new TrayBubbleWrapper(tray, bubble_view));
bubble->InitializeContents(bubble_view);
}
message_center::MessageBubbleBase* bubble() const { return bubble_.get(); }
// Convenience accessors.
views::TrayBubbleView* bubble_view() const { return bubble_->bubble_view(); }
private:
std::unique_ptr<message_center::MessageBubbleBase> bubble_;
std::unique_ptr<TrayBubbleWrapper> bubble_wrapper_;
DISALLOW_COPY_AND_ASSIGN(WebNotificationBubbleWrapper);
};
class WebNotificationItem : public views::View, public gfx::AnimationDelegate {
public:
WebNotificationItem(gfx::AnimationContainer* container,
WebNotificationTray* tray)
: tray_(tray) {
SetPaintToLayer(true);
layer()->SetFillsBoundsOpaquely(false);
views::View::SetVisible(false);
set_owned_by_client();
SetLayoutManager(new views::FillLayout);
animation_.reset(new 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 GetPreferredSize() 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 (IsHorizontalLayout()) {
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();
}
bool IsHorizontalLayout() const {
return IsHorizontalAlignment(tray_->shelf_alignment());
}
private:
// gfx::AnimationDelegate:
void AnimationProgressed(const gfx::Animation* animation) override {
gfx::Transform transform;
if (IsHorizontalLayout()) {
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;
WebNotificationTray* tray_;
DISALLOW_COPY_AND_ASSIGN(WebNotificationItem);
};
class WebNotificationImage : public WebNotificationItem {
public:
WebNotificationImage(const gfx::ImageSkia& image,
const gfx::Size& size,
gfx::AnimationContainer* container,
WebNotificationTray* tray)
: WebNotificationItem(container, tray) {
view_ = new views::ImageView();
view_->SetImage(image);
view_->SetImageSize(size);
AddChildView(view_);
}
private:
views::ImageView* view_;
DISALLOW_COPY_AND_ASSIGN(WebNotificationImage);
};
class WebNotificationLabel : public WebNotificationItem {
public:
WebNotificationLabel(gfx::AnimationContainer* container,
WebNotificationTray* 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) {
if (!base::i18n::IsRTL())
str = base::ASCIIToUTF16("+") + str;
else
str = str + base::ASCIIToUTF16("+");
}
view_->SetText(str);
view_->SetEnabledColor(kWebNotificationColorWithUnread);
SchedulePaint();
}
private:
views::Label* view_;
DISALLOW_COPY_AND_ASSIGN(WebNotificationLabel);
};
WebNotificationTray::WebNotificationTray(WmShelf* shelf,
WmWindow* status_area_window,
SystemTray* system_tray)
: TrayBackgroundView(shelf),
status_area_window_(status_area_window),
system_tray_(system_tray),
show_message_center_on_unlock_(false),
should_update_tray_content_(false),
should_block_shelf_auto_hide_(false) {
DCHECK(shelf);
DCHECK(status_area_window_);
DCHECK(system_tray_);
if (MaterialDesignController::IsShelfMaterial()) {
SetInkDropMode(InkDropMode::ON);
SetContentsBackground(false);
gfx::ImageSkia bell_image =
CreateVectorIcon(kShelfNotificationsIcon, kShelfIconColor);
const gfx::Size bell_icon_size = kTrayItemInnerIconSize;
bell_icon_.reset(new WebNotificationImage(
bell_image, bell_icon_size, animation_container_.get(), this));
} else {
SetContentsBackground(true);
gfx::ImageSkia bell_image =
CreateVectorIcon(gfx::VectorIconId::NOTIFICATIONS, kNoUnreadIconSize,
kWebNotificationColorNoUnread);
const gfx::Size bell_icon_size = kTrayItemInnerBellIconSizeNonMd;
bell_icon_.reset(new WebNotificationImage(
bell_image, bell_icon_size, animation_container_.get(), this));
}
tray_container()->AddChildView(bell_icon_.get());
counter_.reset(new WebNotificationLabel(animation_container_.get(), this));
tray_container()->AddChildView(counter_.get());
message_center_tray_.reset(new message_center::MessageCenterTray(
this, message_center::MessageCenter::Get()));
popup_alignment_delegate_.reset(new AshPopupAlignmentDelegate(shelf));
popup_collection_.reset(new message_center::MessagePopupCollection(
message_center(), message_center_tray_.get(),
popup_alignment_delegate_.get()));
const display::Display& display =
status_area_window_->GetDisplayNearestWindow();
popup_alignment_delegate_->StartObserving(display::Screen::GetScreen(),
display);
OnMessageCenterTrayChanged();
tray_container()->SetMargin(kTrayMainAxisInset, kTrayCrossAxisInset);
}
WebNotificationTray::~WebNotificationTray() {
// Release any child views that might have back pointers before ~View().
message_center_bubble_.reset();
popup_alignment_delegate_.reset();
popup_collection_.reset();
}
// static
void WebNotificationTray::DisableAnimationsForTest(bool disable) {
disable_animations_for_test = disable;
}
// Public methods.
bool WebNotificationTray::ShowMessageCenterInternal(bool show_settings) {
if (!ShouldShowMessageCenter())
return false;
should_block_shelf_auto_hide_ = true;
message_center::MessageCenterBubble* message_center_bubble =
new message_center::MessageCenterBubble(message_center(),
message_center_tray_.get());
int max_height;
if (IsHorizontalAlignment(shelf_alignment())) {
max_height = shelf()->GetIdealBounds().y();
} else {
// Assume the status area and bubble bottoms are aligned when vertical.
gfx::Rect bounds_in_root =
status_area_window_->GetRootWindow()->ConvertRectFromScreen(
status_area_window_->GetBoundsInScreen());
max_height = bounds_in_root.bottom();
}
message_center_bubble->SetMaxHeight(
std::max(0, max_height - GetTrayConstant(TRAY_SPACING)));
if (show_settings)
message_center_bubble->SetSettingsVisible();
message_center_bubble_.reset(
new WebNotificationBubbleWrapper(this, message_center_bubble));
system_tray_->SetHideNotifications(true);
shelf()->UpdateAutoHideState();
SetIsActive(true);
return true;
}
bool WebNotificationTray::ShowMessageCenter() {
return ShowMessageCenterInternal(false /* show_settings */);
}
void WebNotificationTray::HideMessageCenter() {
if (!message_center_bubble())
return;
SetIsActive(false);
message_center_bubble_.reset();
should_block_shelf_auto_hide_ = false;
show_message_center_on_unlock_ = false;
system_tray_->SetHideNotifications(false);
shelf()->UpdateAutoHideState();
}
void WebNotificationTray::SetTrayBubbleHeight(int height) {
popup_alignment_delegate_->SetTrayBubbleHeight(height);
}
int WebNotificationTray::tray_bubble_height_for_test() const {
return popup_alignment_delegate_->tray_bubble_height_for_test();
}
bool WebNotificationTray::ShowPopups() {
if (message_center_bubble())
return false;
popup_collection_->DoUpdateIfPossible();
return true;
}
void WebNotificationTray::HidePopups() {
DCHECK(popup_collection_.get());
popup_collection_->MarkAllPopupsShown();
}
// Private methods.
bool WebNotificationTray::ShouldShowMessageCenter() {
return WmShell::Get()->system_tray_delegate()->ShouldShowNotificationTray() &&
!system_tray_->HasNotificationBubble();
}
bool WebNotificationTray::ShouldBlockShelfAutoHide() const {
return should_block_shelf_auto_hide_;
}
bool WebNotificationTray::IsMessageCenterBubbleVisible() const {
return (message_center_bubble() &&
message_center_bubble()->bubble()->IsVisible());
}
bool WebNotificationTray::IsMouseInNotificationBubble() const {
return false;
}
void WebNotificationTray::ShowMessageCenterBubble() {
if (!IsMessageCenterBubbleVisible())
message_center_tray_->ShowMessageCenterBubble();
}
void WebNotificationTray::UpdateAfterLoginStatusChange(
LoginStatus login_status) {
message_center()->SetLockedState(login_status == LoginStatus::LOCKED);
OnMessageCenterTrayChanged();
}
void WebNotificationTray::SetShelfAlignment(ShelfAlignment alignment) {
if (alignment == shelf_alignment())
return;
TrayBackgroundView::SetShelfAlignment(alignment);
// Destroy any existing bubble so that it will be rebuilt correctly.
message_center_tray_->HideMessageCenterBubble();
message_center_tray_->HidePopupBubble();
}
void WebNotificationTray::AnchorUpdated() {
if (message_center_bubble()) {
message_center_bubble()->bubble_view()->UpdateBubble();
UpdateBubbleViewArrow(message_center_bubble()->bubble_view());
}
}
base::string16 WebNotificationTray::GetAccessibleNameForTray() {
return l10n_util::GetStringUTF16(IDS_MESSAGE_CENTER_ACCESSIBLE_NAME);
}
void WebNotificationTray::HideBubbleWithView(
const views::TrayBubbleView* bubble_view) {
if (message_center_bubble() &&
bubble_view == message_center_bubble()->bubble_view()) {
message_center_tray_->HideMessageCenterBubble();
} else if (popup_collection_.get()) {
message_center_tray_->HidePopupBubble();
}
}
bool WebNotificationTray::PerformAction(const ui::Event& event) {
if (message_center_bubble())
message_center_tray_->HideMessageCenterBubble();
else
message_center_tray_->ShowMessageCenterBubble();
return true;
}
void WebNotificationTray::BubbleViewDestroyed() {
if (message_center_bubble())
message_center_bubble()->bubble()->BubbleViewDestroyed();
}
void WebNotificationTray::OnMouseEnteredView() {}
void WebNotificationTray::OnMouseExitedView() {}
base::string16 WebNotificationTray::GetAccessibleNameForBubble() {
return GetAccessibleNameForTray();
}
void WebNotificationTray::OnBeforeBubbleWidgetInit(
views::Widget* anchor_widget,
views::Widget* bubble_widget,
views::Widget::InitParams* params) const {
// Place the bubble in the same root window as |anchor_widget|.
WmLookup::Get()
->GetWindowForWidget(anchor_widget)
->GetRootWindowController()
->ConfigureWidgetInitParamsForContainer(
bubble_widget, kShellWindowId_SettingBubbleContainer, params);
}
void WebNotificationTray::HideBubble(const views::TrayBubbleView* bubble_view) {
HideBubbleWithView(bubble_view);
}
bool WebNotificationTray::ShowNotifierSettings() {
if (message_center_bubble()) {
static_cast<message_center::MessageCenterBubble*>(
message_center_bubble()->bubble())
->SetSettingsVisible();
return true;
}
return ShowMessageCenterInternal(true /* show_settings */);
}
bool WebNotificationTray::IsContextMenuEnabled() const {
return IsLoggedIn();
}
message_center::MessageCenterTray* WebNotificationTray::GetMessageCenterTray() {
return message_center_tray_.get();
}
bool WebNotificationTray::IsCommandIdChecked(int command_id) const {
if (command_id != kToggleQuietMode)
return false;
return message_center()->IsQuietMode();
}
bool WebNotificationTray::IsCommandIdEnabled(int command_id) const {
return true;
}
void WebNotificationTray::ExecuteCommand(int command_id, int event_flags) {
if (command_id == kToggleQuietMode) {
bool in_quiet_mode = message_center()->IsQuietMode();
message_center()->SetQuietMode(!in_quiet_mode);
return;
}
base::TimeDelta expires_in = command_id == kEnableQuietModeDay
? base::TimeDelta::FromDays(1)
: base::TimeDelta::FromHours(1);
message_center()->EnterQuietModeWithExpire(expires_in);
}
void WebNotificationTray::OnMessageCenterTrayChanged() {
// 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::Bind(&WebNotificationTray::UpdateTrayContent, AsWeakPtr()));
}
void WebNotificationTray::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_tray_->message_center();
size_t visible_small_icon_count = 0;
for (const auto* notification : message_center->GetVisibleNotifications()) {
gfx::Image image = notification->small_image();
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 =
new WebNotificationImage(image.AsImageSkia(), kTrayItemInnerIconSize,
animation_container_.get(), this);
visible_small_icons_.insert(std::make_pair(notification->id(), item));
tray_container()->AddChildViewAt(item, 0);
item->SetVisible(true);
}
// Remove unnecessary icons.
for (const std::string& id : notification_ids) {
WebNotificationImage* item = visible_small_icons_[id];
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);
// 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(IsLoggedIn());
PreferredSizeChanged();
Layout();
SchedulePaint();
if (IsLoggedIn())
system_tray_->SetNextFocusableView(this);
}
void WebNotificationTray::ClickedOutsideBubble() {
// Only hide the message center
if (!message_center_bubble())
return;
message_center_tray_->HideMessageCenterBubble();
}
message_center::MessageCenter* WebNotificationTray::message_center() const {
return message_center_tray_->message_center();
}
bool WebNotificationTray::IsLoggedIn() const {
WmShell* shell = WmShell::Get();
return shell->system_tray_delegate()->GetUserLoginStatus() !=
LoginStatus::NOT_LOGGED_IN &&
!shell->GetSessionStateDelegate()->IsInSecondaryLoginScreen();
}
// Methods for testing
bool WebNotificationTray::IsPopupVisible() const {
return message_center_tray_->popups_visible();
}
message_center::MessageCenterBubble*
WebNotificationTray::GetMessageCenterBubbleForTest() {
if (!message_center_bubble())
return nullptr;
return static_cast<message_center::MessageCenterBubble*>(
message_center_bubble()->bubble());
}
} // namespace ash