blob: b9320369797019306c5e2921c7d38c16e82a77ef [file] [log] [blame]
// Copyright (c) 2013 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/message_center/message_center_view.h"
#include <list>
#include <map>
#include "ash/message_center/message_center_button_bar.h"
#include "ash/message_center/message_center_style.h"
#include "ash/message_center/notifier_settings_view.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/session/session_controller.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "base/message_loop/message_loop.h"
#include "base/stl_util.h"
#include "build/build_config.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/message_center_types.h"
#include "ui/message_center/public/cpp/message_center_constants.h"
#include "ui/message_center/ui_controller.h"
#include "ui/message_center/views/message_view.h"
#include "ui/message_center/views/message_view_factory.h"
#include "ui/message_center/views/notification_control_buttons_view.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/scrollbar/overlay_scroll_bar.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/widget/widget.h"
using message_center::MessageCenter;
using message_center::MessageView;
using message_center::Notification;
using message_center::NotificationList;
namespace ash {
// static
const SkColor MessageCenterView::kBackgroundColor =
SkColorSetARGB(0xF2, 0xf0, 0xf0, 0xf2);
// static
const size_t MessageCenterView::kMaxVisibleNotifications = 100;
// static
bool MessageCenterView::disable_animation_for_testing = false;
namespace {
constexpr int kMinScrollViewHeight = 77;
constexpr int kEmptyViewHeight = 96;
constexpr gfx::Insets kEmptyViewPadding(0, 0, 24, 0);
void SetViewHierarchyEnabled(views::View* view, bool enabled) {
for (int i = 0; i < view->child_count(); i++)
SetViewHierarchyEnabled(view->child_at(i), enabled);
view->SetEnabled(enabled);
}
// View that is shown when there are no notifications.
class EmptyNotificationView : public views::View {
public:
EmptyNotificationView() {
views::BoxLayout* layout =
new views::BoxLayout(views::BoxLayout::kVertical, kEmptyViewPadding, 0);
layout->set_main_axis_alignment(views::BoxLayout::MAIN_AXIS_ALIGNMENT_END);
layout->set_cross_axis_alignment(
views::BoxLayout::CROSS_AXIS_ALIGNMENT_CENTER);
SetLayoutManager(layout);
views::ImageView* icon = new views::ImageView();
icon->SetImage(gfx::CreateVectorIcon(
kNotificationCenterAllDoneIcon, message_center_style::kEmptyIconSize,
message_center_style::kEmptyViewColor));
icon->SetBorder(
views::CreateEmptyBorder(message_center_style::kEmptyIconPadding));
AddChildView(icon);
views::Label* label = new views::Label(
l10n_util::GetStringUTF16(IDS_ASH_MESSAGE_CENTER_NO_MESSAGES));
label->SetEnabledColor(message_center_style::kEmptyViewColor);
// "Roboto-Medium, 12sp" is specified in the mock.
label->SetFontList(message_center_style::GetFontListForSizeAndWeight(
message_center_style::kEmptyLabelSize, gfx::Font::Weight::MEDIUM));
label->SetHorizontalAlignment(gfx::ALIGN_CENTER);
AddChildView(label);
}
// views::View:
int GetHeightForWidth(int w) const override { return kEmptyViewHeight; }
private:
DISALLOW_COPY_AND_ASSIGN(EmptyNotificationView);
};
} // namespace
// MessageCenterView ///////////////////////////////////////////////////////////
MessageCenterView::MessageCenterView(
MessageCenter* message_center,
message_center::UiController* ui_controller,
int max_height,
bool initially_settings_visible)
: message_center_(message_center),
ui_controller_(ui_controller),
settings_visible_(initially_settings_visible),
is_locked_(Shell::Get()->session_controller()->IsScreenLocked()) {
if (is_locked_)
mode_ = Mode::LOCKED;
else if (initially_settings_visible)
mode_ = Mode::SETTINGS;
message_center_->AddObserver(this);
set_notify_enter_exit_on_child(true);
SetBackground(views::CreateSolidBackground(kBackgroundColor));
SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
button_bar_ = new MessageCenterButtonBar(
this, message_center, initially_settings_visible, GetButtonBarTitle());
button_bar_->SetCloseAllButtonEnabled(false);
const int button_height = button_bar_->GetPreferredSize().height();
scroller_ = new views::ScrollView();
scroller_->SetBackgroundColor(kBackgroundColor);
scroller_->ClipHeightTo(kMinScrollViewHeight, max_height - button_height);
scroller_->SetVerticalScrollBar(new views::OverlayScrollBar(false));
scroller_->SetHorizontalScrollBar(new views::OverlayScrollBar(true));
message_list_view_.reset(new MessageListView());
message_list_view_->set_scroller(scroller_);
message_list_view_->set_owned_by_client();
message_list_view_->AddObserver(this);
// We want to swap the contents of the scroll view between the empty list
// view and the message list view, without constructing them afresh each
// time. So, since the scroll view deletes old contents each time you
// set the contents (regardless of the |owned_by_client_| setting) we need
// an intermediate view for the contents whose children we can swap in and
// out.
views::View* scroller_contents = new views::View();
scroller_contents->SetLayoutManager(new views::FillLayout());
scroller_contents->AddChildView(message_list_view_.get());
scroller_->SetContents(scroller_contents);
settings_view_ = new NotifierSettingsView();
no_notifications_view_ = new EmptyNotificationView();
scroller_->SetVisible(false); // Because it has no notifications at first.
settings_view_->SetVisible(mode_ == Mode::SETTINGS);
no_notifications_view_->SetVisible(mode_ == Mode::NO_NOTIFICATIONS);
AddChildView(scroller_);
AddChildView(settings_view_);
AddChildView(no_notifications_view_);
AddChildView(button_bar_);
}
MessageCenterView::~MessageCenterView() {
message_list_view_->RemoveObserver(this);
if (!is_closing_)
message_center_->RemoveObserver(this);
if (focus_manager_)
focus_manager_->RemoveFocusChangeListener(this);
}
void MessageCenterView::Init() {
focus_manager_ = GetFocusManager();
if (focus_manager_)
focus_manager_->AddFocusChangeListener(this);
}
void MessageCenterView::SetNotifications(
const NotificationList::Notifications& notifications) {
if (is_closing_)
return;
int index = 0;
for (NotificationList::Notifications::const_iterator iter =
notifications.begin();
iter != notifications.end(); ++iter) {
AddNotificationAt(*(*iter), index++);
message_center_->DisplayedNotification(
(*iter)->id(), message_center::DISPLAY_SOURCE_MESSAGE_CENTER);
if (message_list_view_->GetNotificationCount() >=
kMaxVisibleNotifications) {
break;
}
}
Update(false /* animate */);
scroller_->RequestFocus();
}
void MessageCenterView::SetSettingsVisible(bool visible) {
settings_visible_ = visible;
Update(true /* animate */);
}
void MessageCenterView::ClearAllClosableNotifications() {
if (is_closing_)
return;
is_clearing_all_notifications_ = true;
UpdateButtonBarStatus();
SetViewHierarchyEnabled(scroller_, false);
message_list_view_->ClearAllClosableNotifications(
scroller_->GetVisibleRect());
}
void MessageCenterView::OnLockStateChanged(bool locked) {
is_locked_ = locked;
Update(true /* animate */);
// Refresh a11y information, because accessible name of the view changes.
NotifyAccessibilityEvent(ui::AX_EVENT_ARIA_ATTRIBUTE_CHANGED, true);
}
void MessageCenterView::OnAllNotificationsCleared() {
SetViewHierarchyEnabled(scroller_, true);
button_bar_->SetCloseAllButtonEnabled(false);
// The status of buttons will be updated after removing all notifications.
// Action by user.
message_center_->RemoveAllNotifications(
true /* by_user */,
message_center::MessageCenter::RemoveType::NON_PINNED);
is_clearing_all_notifications_ = false;
}
size_t MessageCenterView::NumMessageViewsForTest() const {
return message_list_view_->GetNotificationCount();
}
void MessageCenterView::OnSettingsChanged() {
scroller_->InvalidateLayout();
PreferredSizeChanged();
Layout();
}
void MessageCenterView::SetIsClosing(bool is_closing) {
is_closing_ = is_closing;
if (is_closing)
message_center_->RemoveObserver(this);
else
message_center_->AddObserver(this);
}
void MessageCenterView::OnDidChangeFocus(views::View* before,
views::View* now) {
// Update the button visibility when the focus state is changed.
size_t count = message_list_view_->GetNotificationCount();
for (size_t i = 0; i < count; ++i) {
MessageView* view = message_list_view_->GetNotificationAt(i);
// ControlButtonsView is not in the same view hierarchy on ARC++
// notifications, so check it separately.
if (view->Contains(before) || view->Contains(now) ||
(view->GetControlButtonsView() &&
(view->GetControlButtonsView()->Contains(before) ||
view->GetControlButtonsView()->Contains(now)))) {
view->UpdateControlButtonsVisibility();
}
// Ensure that a notification is not removed or added during iteration.
DCHECK_EQ(count, message_list_view_->GetNotificationCount());
}
}
void MessageCenterView::Layout() {
if (is_closing_)
return;
int button_height = button_bar_->GetHeightForWidth(width());
int settings_height =
std::min(GetSettingsHeightForWidth(width()), height() - button_height);
// In order to keep the fix for https://crbug.com/767805 working,
// we have to always call SetBounds of scroller_.
// TODO(tetsui): Fix the bug above without calling SetBounds, as SetBounds
// invokes Layout() which is a heavy operation.
scroller_->SetBounds(0, 0, width(), height() - button_height);
if (settings_view_->visible()) {
settings_view_->SetBounds(0, height() - settings_height, width(),
settings_height);
}
if (no_notifications_view_->visible())
no_notifications_view_->SetBounds(0, 0, width(), kEmptyViewHeight);
button_bar_->SetBounds(0, height() - button_height - settings_height, width(),
button_height);
if (GetWidget())
GetWidget()->GetRootView()->SchedulePaint();
}
gfx::Size MessageCenterView::CalculatePreferredSize() const {
int width = 0;
for (int i = 0; i < child_count(); ++i) {
const views::View* child = child_at(0);
if (child->visible())
width = std::max(width, child->GetPreferredSize().width());
}
return gfx::Size(width, GetHeightForWidth(width));
}
int MessageCenterView::GetHeightForWidth(int width) const {
if (settings_transition_animation_ &&
settings_transition_animation_->is_animating()) {
return button_bar_->GetHeightForWidth(width) +
GetContentHeightDuringAnimation(width);
}
int content_height = 0;
if (mode_ == Mode::NOTIFICATIONS)
content_height += scroller_->GetHeightForWidth(width);
else if (mode_ == Mode::SETTINGS)
content_height += settings_view_->GetHeightForWidth(width);
else if (no_notifications_view_->visible())
content_height += no_notifications_view_->GetHeightForWidth(width);
return button_bar_->GetHeightForWidth(width) + content_height;
}
bool MessageCenterView::OnMouseWheel(const ui::MouseWheelEvent& event) {
// Do not rely on the default scroll event handler of ScrollView because
// the scroll happens only when the focus is on the ScrollView. The
// notification center will allow the scrolling even when the focus is on
// the buttons.
if (scroller_->bounds().Contains(event.location()))
return scroller_->OnMouseWheel(event);
return views::View::OnMouseWheel(event);
}
void MessageCenterView::OnMouseExited(const ui::MouseEvent& event) {
if (is_closing_)
return;
message_list_view_->ResetRepositionSession();
Update(true /* animate */);
}
void MessageCenterView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
node_data->role = ui::AX_ROLE_DIALOG;
node_data->SetName(GetButtonBarTitle());
}
void MessageCenterView::OnNotificationAdded(const std::string& id) {
int index = 0;
const NotificationList::Notifications& notifications =
message_center_->GetVisibleNotifications();
for (NotificationList::Notifications::const_iterator
iter = notifications.begin();
iter != notifications.end(); ++iter, ++index) {
if ((*iter)->id() == id) {
AddNotificationAt(*(*iter), index);
break;
}
if (message_list_view_->GetNotificationCount() >=
kMaxVisibleNotifications) {
break;
}
}
Update(true /* animate */);
}
void MessageCenterView::OnNotificationRemoved(const std::string& id,
bool by_user) {
auto view_pair = message_list_view_->GetNotificationById(id);
MessageView* view = view_pair.second;
if (!view)
return;
size_t index = view_pair.first;
// We skip repositioning during clear-all anomation, since we don't need keep
// positions.
if (by_user && !is_clearing_all_notifications_) {
message_list_view_->SetRepositionTarget(view->bounds());
// Moves the keyboard focus to the next notification if the removed
// notification is focused so that the user can dismiss notifications
// without re-focusing by tab key.
if (view->IsCloseButtonFocused() || view->HasFocus()) {
views::View* next_focused_view = nullptr;
if (message_list_view_->GetNotificationCount() > index + 1)
next_focused_view = message_list_view_->GetNotificationAt(index + 1);
else if (index > 0)
next_focused_view = message_list_view_->GetNotificationAt(index - 1);
if (next_focused_view) {
if (view->IsCloseButtonFocused()) {
// Safe cast since all views in MessageListView are MessageViews.
static_cast<MessageView*>(next_focused_view)
->RequestFocusOnCloseButton();
} else {
next_focused_view->RequestFocus();
}
}
}
}
message_list_view_->RemoveNotification(view);
Update(true /* animate */);
}
// This is a separate function so we can override it in tests.
bool MessageCenterView::SetRepositionTarget() {
// Set the item on the mouse cursor as the reposition target so that it
// should stick to the current position over the update.
if (message_list_view_->IsMouseHovered()) {
size_t count = message_list_view_->GetNotificationCount();
for (size_t i = 0; i < count; ++i) {
const views::View* hover_view = message_list_view_->GetNotificationAt(i);
if (hover_view->IsMouseHovered()) {
message_list_view_->SetRepositionTarget(hover_view->bounds());
return true;
}
}
}
return false;
}
void MessageCenterView::OnNotificationUpdated(const std::string& id) {
// If there is no reposition target anymore, make sure to reset the reposition
// session.
if (!SetRepositionTarget())
message_list_view_->ResetRepositionSession();
UpdateNotification(id);
}
void MessageCenterView::OnQuietModeChanged(bool is_quiet_mode) {
settings_view_->SetQuietModeState(is_quiet_mode);
button_bar_->SetQuietModeState(is_quiet_mode);
}
void MessageCenterView::ClickOnNotification(
const std::string& notification_id) {
message_center_->ClickOnNotification(notification_id);
}
void MessageCenterView::RemoveNotification(const std::string& notification_id,
bool by_user) {
message_center_->RemoveNotification(notification_id, by_user);
}
void MessageCenterView::ClickOnNotificationButton(
const std::string& notification_id,
int button_index) {
message_center_->ClickOnNotificationButton(notification_id, button_index);
}
void MessageCenterView::ClickOnNotificationButtonWithReply(
const std::string& notification_id,
int button_index,
const base::string16& reply) {
message_center_->ClickOnNotificationButtonWithReply(notification_id,
button_index, reply);
}
void MessageCenterView::ClickOnSettingsButton(
const std::string& notification_id) {
message_center_->ClickOnSettingsButton(notification_id);
}
void MessageCenterView::UpdateNotificationSize(
const std::string& notification_id) {
// TODO(edcourtney, yoshiki): We don't call OnNotificationUpdated directly
// because it resets the reposition session, which can end up deleting
// notification items when it cancels animations. This causes problems for
// ARC notifications. See crbug.com/714493. OnNotificationUpdated should not
// have to consider the reposition session, but OnMouseEntered and
// OnMouseExited don't work properly for ARC notifications at the moment.
// See crbug.com/714587.
UpdateNotification(notification_id);
}
void MessageCenterView::AnimationEnded(const gfx::Animation* animation) {
DCHECK_EQ(animation, settings_transition_animation_.get());
message_center::Visibility visibility =
mode_ == Mode::SETTINGS ? message_center::VISIBILITY_SETTINGS
: message_center::VISIBILITY_MESSAGE_CENTER;
message_center_->SetVisibility(visibility);
if (source_view_) {
source_view_->SetVisible(false);
}
if (target_view_)
target_view_->SetVisible(true);
if (settings_transition_animation_)
NotifyAnimationState(false /* animating */);
settings_transition_animation_.reset();
PreferredSizeChanged();
Layout();
// We should update minimum fixed height based on new |scroller_| height.
// This is required when switching between message list and settings panel.
if (!scroller_->visible())
message_list_view_->ResetRepositionSession();
}
void MessageCenterView::AnimationProgressed(const gfx::Animation* animation) {
DCHECK_EQ(animation, settings_transition_animation_.get());
PreferredSizeChanged();
Layout();
SchedulePaint();
}
void MessageCenterView::AnimationCanceled(const gfx::Animation* animation) {
DCHECK_EQ(animation, settings_transition_animation_.get());
AnimationEnded(animation);
}
void MessageCenterView::AddNotificationAt(const Notification& notification,
int index) {
MessageView* view = message_center::MessageViewFactory::Create(
this, notification, false); // Not top-level.
// TODO(yoshiki): Temporarily disable context menu on custom (arc)
// notifications. See crbug.com/750307 for details.
if (notification.type() != message_center::NOTIFICATION_TYPE_CUSTOM &&
notification.should_show_settings_button()) {
view->set_context_menu_controller(&context_menu_controller_);
}
view->set_scroller(scroller_);
message_list_view_->AddNotificationAt(view, index);
}
base::string16 MessageCenterView::GetButtonBarTitle() const {
if (is_locked_)
return l10n_util::GetStringUTF16(IDS_ASH_MESSAGE_CENTER_FOOTER_LOCKSCREEN);
return l10n_util::GetStringUTF16(IDS_ASH_MESSAGE_CENTER_FOOTER_TITLE);
}
void MessageCenterView::Update(bool animate) {
bool no_message_views = (message_list_view_->GetNotificationCount() == 0);
if (is_locked_)
SetVisibilityMode(Mode::LOCKED, animate);
else if (settings_visible_)
SetVisibilityMode(Mode::SETTINGS, animate);
else if (no_message_views)
SetVisibilityMode(Mode::NO_NOTIFICATIONS, animate);
else
SetVisibilityMode(Mode::NOTIFICATIONS, animate);
if (no_message_views) {
scroller_->SetFocusBehavior(FocusBehavior::NEVER);
} else {
#if defined(OS_MACOSX)
scroller_->SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
#else
scroller_->SetFocusBehavior(FocusBehavior::ALWAYS);
#endif
}
UpdateButtonBarStatus();
if (scroller_->visible())
scroller_->InvalidateLayout();
PreferredSizeChanged();
Layout();
}
void MessageCenterView::SetVisibilityMode(Mode mode, bool animate) {
if (is_closing_)
return;
if (mode == mode_)
return;
if (mode_ == Mode::NOTIFICATIONS)
source_view_ = scroller_;
else if (mode_ == Mode::SETTINGS)
source_view_ = settings_view_;
else if (mode_ == Mode::NO_NOTIFICATIONS)
source_view_ = no_notifications_view_;
else
source_view_ = nullptr;
if (mode == Mode::NOTIFICATIONS)
target_view_ = scroller_;
else if (mode == Mode::SETTINGS)
target_view_ = settings_view_;
else if (mode == Mode::NO_NOTIFICATIONS)
target_view_ = no_notifications_view_;
else
target_view_ = nullptr;
mode_ = mode;
source_height_ = source_view_ ? source_view_->GetHeightForWidth(width()) : 0;
target_height_ = target_view_ ? target_view_->GetHeightForWidth(width()) : 0;
if (source_view_)
source_view_->SetVisible(true);
if (target_view_)
target_view_->SetVisible(true);
if (!animate || disable_animation_for_testing) {
AnimationEnded(nullptr);
return;
}
NotifyAnimationState(true /* animating */);
settings_transition_animation_ = std::make_unique<gfx::SlideAnimation>(this);
settings_transition_animation_->SetSlideDuration(
message_center_style::kSettingsTransitionDurationMs);
settings_transition_animation_->SetTweenType(gfx::Tween::EASE_IN_OUT);
settings_transition_animation_->Show();
}
void MessageCenterView::UpdateButtonBarStatus() {
// Disables all buttons during animation of cleaning of all notifications.
if (is_clearing_all_notifications_) {
button_bar_->SetSettingsAndQuietModeButtonsEnabled(false);
button_bar_->SetCloseAllButtonEnabled(false);
return;
}
button_bar_->SetBackArrowVisible(mode_ == Mode::SETTINGS);
button_bar_->SetButtonsVisible(!is_locked_);
button_bar_->SetTitle(GetButtonBarTitle());
if (!is_locked_)
EnableCloseAllIfAppropriate();
}
void MessageCenterView::EnableCloseAllIfAppropriate() {
if (mode_ == Mode::NOTIFICATIONS) {
bool no_closable_views = true;
size_t count = message_list_view_->GetNotificationCount();
for (size_t i = 0; i < count; ++i) {
if (!message_list_view_->GetNotificationAt(i)->GetPinned()) {
no_closable_views = false;
break;
}
}
button_bar_->SetCloseAllButtonEnabled(!no_closable_views);
} else {
// Disable the close-all button since no notification is visible.
button_bar_->SetCloseAllButtonEnabled(false);
}
}
void MessageCenterView::SetNotificationViewForTest(MessageView* view) {
message_list_view_->AddNotificationAt(view, 0);
}
void MessageCenterView::UpdateNotification(const std::string& id) {
MessageView* view = message_list_view_->GetNotificationById(id).second;
if (!view)
return;
Notification* notification = message_center_->FindVisibleNotificationById(id);
if (notification) {
int old_width = view->width();
int old_height = view->height();
bool old_pinned = view->GetPinned();
message_list_view_->UpdateNotification(view, *notification);
if (view->GetHeightForWidth(old_width) != old_height) {
Update(true /* animate */);
} else if (view->GetPinned() != old_pinned) {
// Animate flag is false, since the pinned flag transition doesn't need
// animation.
Update(false /* animate */);
}
}
// Notify accessibility that the contents have changed.
view->NotifyAccessibilityEvent(ui::AX_EVENT_CHILDREN_CHANGED, false);
}
void MessageCenterView::NotifyAnimationState(bool animating) {
size_t count = message_list_view_->GetNotificationCount();
for (size_t i = 0; i < count; ++i) {
MessageView* view = message_list_view_->GetNotificationAt(i);
if (animating)
view->OnContainerAnimationStarted();
else
view->OnContainerAnimationEnded();
// Ensure that a notification is not removed or added during iteration.
DCHECK_EQ(count, message_list_view_->GetNotificationCount());
}
}
int MessageCenterView::GetSettingsHeightForWidth(int width) const {
if (settings_transition_animation_ &&
settings_transition_animation_->is_animating() &&
(source_view_ == settings_view_ || target_view_ == settings_view_)) {
return settings_transition_animation_->CurrentValueBetween(
target_view_ == settings_view_ ? 0 : source_height_,
source_view_ == settings_view_ ? 0 : target_height_);
} else {
return mode_ == Mode::SETTINGS ? settings_view_->GetHeightForWidth(width)
: 0;
}
}
int MessageCenterView::GetContentHeightDuringAnimation(int width) const {
DCHECK(settings_transition_animation_);
int content_height = settings_transition_animation_->CurrentValueBetween(
target_view_ == settings_view_ ? 0 : source_height_,
source_view_ == settings_view_ ? 0 : target_height_);
if (target_view_ == settings_view_)
content_height = std::max(source_height_, content_height);
if (source_view_ == settings_view_)
content_height = std::max(target_height_, content_height);
return content_height;
}
} // namespace ash