Commit 9099fd99 authored by Alex Newcomer's avatar Alex Newcomer Committed by Commit Bot

cros: Add notifications to context menus

Only the most recent notification will be shown.

Adding a few new components:
 - NotificationMenuController
   - Handles adding/removing the container MenuItemView to an app
     context menu, and passing notification data to
     NotificationMenuView.
 - NotificationMenuView
   - Handles showing NotificationItemViews, showing the proper
     notifications, and in the future showing the overflow area.
 - NotificationItemView
   - Shows basic notification data.
 - NotificationMenuHeaderView
   - Shows "Notifications" text and a counter showing the number
     of notifications.

Please see the design doc and spec, linked in the code review request.

Bug: 801015
Change-Id: I907794db5bcf90e5238773ca7cf9978d539b0855
Reviewed-on: https://chromium-review.googlesource.com/1095431
Commit-Queue: Alex Newcomer <newcomer@chromium.org>
Reviewed-by: default avatarXiyuan Xia <xiyuan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#568331}
parent 2d5ed910
......@@ -1313,6 +1313,7 @@ component("ash") {
"//ui/wm",
]
deps = [
"//ash/app_menu",
"//ash/autoclick/common:autoclick",
"//ash/components/autoclick/public/mojom",
"//ash/components/cursor",
......@@ -1669,6 +1670,8 @@ test("ash_unittests") {
"app_list/model/app_list_item_list_unittest.cc",
"app_list/model/app_list_model_unittest.cc",
"app_list/presenter/app_list_presenter_impl_unittest.cc",
"app_menu/notification_menu_controller_unittest.cc",
"app_menu/notification_menu_view_unittest.cc",
"assistant/assistant_controller_unittest.cc",
"autoclick/autoclick_unittest.cc",
"cursor_unittest.cc",
......@@ -1943,6 +1946,7 @@ test("ash_unittests") {
":test_support_without_content",
"//ash/app_list:test_support",
"//ash/app_list/presenter",
"//ash/app_menu",
"//ash/autoclick/common:autoclick",
"//ash/components/fast_ink",
"//ash/components/fast_ink:unit_tests",
......@@ -2162,6 +2166,8 @@ static_library("test_support_common") {
"mojo_test_interface_factory.h",
# TODO(jamescook): Consider adding a //ash/public/cpp:test_support target.
"app_menu/notification_menu_view_test_api.cc",
"app_menu/notification_menu_view_test_api.h",
"public/cpp/immersive/immersive_fullscreen_controller_test_api.cc",
"public/cpp/immersive/immersive_fullscreen_controller_test_api.h",
"rotator/screen_rotation_animator_test_api.cc",
......@@ -2252,6 +2258,7 @@ static_library("test_support_common") {
"//ash",
"//ash/app_list:test_support",
"//ash/app_list/presenter",
"//ash/app_menu",
"//ash/components/fast_ink",
"//ash/public/cpp",
"//ash/public/interfaces:test_interfaces",
......
......@@ -96,6 +96,7 @@ component("app_list") {
deps = [
"//ash/app_list/resources",
"//ash/app_menu",
"//ash/public/cpp/app_list/vector_icons",
"//base",
"//base:i18n",
......
......@@ -10,7 +10,7 @@
#include <vector>
#include "ash/app_list/app_list_export.h"
#include "ash/public/cpp/app_menu_model_adapter.h"
#include "ash/app_menu/app_menu_model_adapter.h"
#include "ash/public/interfaces/menu.mojom.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/base/ui_base_types.h"
......
# Copyright 2018 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.
import("//build/config/ui.gni")
assert(is_chromeos)
component("app_menu") {
sources = [
"app_menu_model_adapter.cc",
"app_menu_model_adapter.h",
"notification_item_view.cc",
"notification_item_view.h",
"notification_menu_controller.cc",
"notification_menu_controller.h",
"notification_menu_header_view.cc",
"notification_menu_header_view.h",
"notification_menu_view.cc",
"notification_menu_view.h",
]
defines = [ "APP_MENU_IMPLEMENTATION" ]
deps = [
"//ash/public/cpp",
"//base",
"//ui/gfx",
"//ui/gfx/geometry",
"//ui/message_center",
"//ui/strings:ui_strings_grit",
"//ui/views",
]
}
// Copyright 2018 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.
#ifndef ASH_APP_MENU_APP_MENU_EXPORT_H_
#define ASH_APP_MENU_APP_MENU_EXPORT_H_
// Defines APP_MENU_EXPORT so that functionality implemented by the app_menu
// module can be exported to consumers.
#if defined(COMPONENT_BUILD)
#if defined(WIN32)
#if defined(APP_MENU_IMPLEMENTATION)
#define APP_MENU_EXPORT __declspec(dllexport)
#else
#define APP_MENU_EXPORT __declspec(dllimport)
#endif // defined(APP_MENU_IMPLEMENTATION)
#else // defined(WIN32)
#if defined(APP_MENU_IMPLEMENTATION)
#define APP_MENU_EXPORT __attribute__((visibility("default")))
#else
#define APP_MENU_EXPORT
#endif
#endif
#else // defined(COMPONENT_BUILD)
#define APP_MENU_EXPORT
#endif
#endif // ASH_APP_MENU_APP_MENU_EXPORT_H_
......@@ -2,10 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/public/cpp/app_menu_model_adapter.h"
#include "ash/app_menu/app_menu_model_adapter.h"
#include "ash/app_menu/notification_menu_controller.h"
#include "base/metrics/histogram_macros.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/base/ui_base_features.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/menu_model_adapter.h"
#include "ui/views/controls/menu/menu_runner.h"
......@@ -35,6 +37,11 @@ void AppMenuModelAdapter::Run(const gfx::Rect& menu_anchor_rect,
menu_open_time_ = base::TimeTicks::Now();
root_ = CreateMenu();
if (features::IsNotificationIndicatorEnabled()) {
notification_menu_controller_ =
std::make_unique<NotificationMenuController>(app_id_, root_,
model_.get());
}
menu_runner_ = std::make_unique<views::MenuRunner>(root_, run_types);
menu_runner_->RunMenuAt(menu_owner_->GetWidget(), nullptr /* MenuButton */,
menu_anchor_rect, menu_anchor_position, source_type_);
......
......@@ -2,13 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef ASH_PUBLIC_CPP_APP_MENU_MODEL_ADAPTER_H_
#define ASH_PUBLIC_CPP_APP_MENU_MODEL_ADAPTER_H_
#ifndef ASH_APP_MENU_APP_MENU_MODEL_ADAPTER_H_
#define ASH_APP_MENU_APP_MENU_MODEL_ADAPTER_H_
#include <memory>
#include <string>
#include "ash/public/cpp/ash_public_export.h"
#include "ash/app_menu/app_menu_export.h"
#include "base/callback.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/controls/menu/menu_model_adapter.h"
......@@ -26,7 +26,9 @@ class View;
namespace ash {
class ASH_PUBLIC_EXPORT AppMenuModelAdapter : public views::MenuModelAdapter {
class NotificationMenuController;
class APP_MENU_EXPORT AppMenuModelAdapter : public views::MenuModelAdapter {
public:
AppMenuModelAdapter(const std::string& app_id,
std::unique_ptr<ui::SimpleMenuModel> model,
......@@ -69,6 +71,10 @@ class ASH_PUBLIC_EXPORT AppMenuModelAdapter : public views::MenuModelAdapter {
// The list of items which will be shown in the menu.
std::unique_ptr<ui::SimpleMenuModel> model_;
// Responsible for adding the container MenuItemView to the parent
// MenuItemView, and adding NOTIFICATION_CONTAINER to the model.
std::unique_ptr<NotificationMenuController> notification_menu_controller_;
// The view showing a context menu. This can be either a ShelfView,
// ShelfButton, or AppListItemView. Not owned.
views::View* const menu_owner_;
......@@ -94,4 +100,4 @@ class ASH_PUBLIC_EXPORT AppMenuModelAdapter : public views::MenuModelAdapter {
} // namespace ash
#endif // ASH_PUBLIC_CPP_APP_MENU_MODEL_ADAPTER_H_
#endif // ASH_APP_MENU_APP_MENU_MODEL_ADAPTER_H_
// Copyright 2018 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/app_menu/notification_item_view.h"
#include "ash/public/cpp/app_menu_constants.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/text_elider.h"
#include "ui/message_center/views/proportional_image_view.h"
#include "ui/views/border.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/menu/menu_config.h"
#include "ui/views/layout/box_layout.h"
namespace ash {
namespace {
// Line height of all text in NotificationItemView in dips.
constexpr int kNotificationItemTextLineHeight = 16;
// Padding of |proportional_icon_view_|.
constexpr int kIconVerticalPadding = 4;
constexpr int kIconHorizontalPadding = 12;
// The size of the icon in NotificationItemView in dips.
constexpr gfx::Size kProportionalIconViewSize(24, 24);
// Text color of NotificationItemView's |message_|.
constexpr SkColor kNotificationMessageTextColor =
SkColorSetARGB(179, 0x5F, 0x63, 0x68);
// Text color of NotificationItemView's |title_|.
constexpr SkColor kNotificationTitleTextColor =
SkColorSetARGB(230, 0x21, 0x23, 0x24);
} // namespace
NotificationItemView::NotificationItemView(const base::string16& title,
const base::string16& message,
const gfx::Image& icon,
const std::string notification_id)
: title_(title), message_(message), notification_id_(notification_id) {
SetBorder(views::CreateEmptyBorder(
gfx::Insets(kNotificationVerticalPadding, kNotificationHorizontalPadding,
kNotificationVerticalPadding, kIconHorizontalPadding)));
text_container_ = new views::View();
text_container_->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
AddChildView(text_container_);
const int maximum_text_length_px =
views::MenuConfig::instance().touchable_menu_width -
kNotificationHorizontalPadding - kIconHorizontalPadding * 2 -
kProportionalIconViewSize.width();
views::Label* title_label = new views::Label(
gfx::ElideText(title_, views::Label::GetDefaultFontList(),
maximum_text_length_px, gfx::ElideBehavior::ELIDE_TAIL));
title_label->SetEnabledColor(kNotificationTitleTextColor);
title_label->SetLineHeight(kNotificationItemTextLineHeight);
title_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
text_container_->AddChildView(title_label);
views::Label* message_label = new views::Label(
gfx::ElideText(message_, views::Label::GetDefaultFontList(),
maximum_text_length_px, gfx::ElideBehavior::ELIDE_TAIL));
message_label->SetEnabledColor(kNotificationMessageTextColor);
message_label->SetLineHeight(kNotificationItemTextLineHeight);
message_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
text_container_->AddChildView(message_label);
proportional_icon_view_ =
new message_center::ProportionalImageView(kProportionalIconViewSize);
AddChildView(proportional_icon_view_);
proportional_icon_view_->SetImage(icon.AsImageSkia(),
kProportionalIconViewSize);
}
NotificationItemView::~NotificationItemView() = default;
gfx::Size NotificationItemView::CalculatePreferredSize() const {
return gfx::Size(views::MenuConfig::instance().touchable_menu_width,
kNotificationItemViewHeight);
}
void NotificationItemView::Layout() {
gfx::Insets insets = GetInsets();
const gfx::Size text_container_preferred_size =
text_container_->GetPreferredSize();
text_container_->SetBounds(insets.left(), insets.top(),
text_container_preferred_size.width(),
text_container_preferred_size.height());
proportional_icon_view_->SetBounds(
width() - insets.right() - kProportionalIconViewSize.width(),
insets.top() + kIconVerticalPadding, kProportionalIconViewSize.width(),
kProportionalIconViewSize.height());
}
} // namespace ash
// Copyright 2018 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.
#ifndef ASH_APP_MENU_NOTIFICATION_ITEM_VIEW_H_
#define ASH_APP_MENU_NOTIFICATION_ITEM_VIEW_H_
#include <string>
#include "ash/app_menu/app_menu_export.h"
#include "base/strings/string16.h"
#include "ui/views/view.h"
namespace gfx {
class Image;
class Size;
} // namespace gfx
namespace message_center {
class ProportionalImageView;
}
namespace ash {
// The view which contains the details of a notification.
class APP_MENU_EXPORT NotificationItemView : public views::View {
public:
NotificationItemView(const base::string16& title,
const base::string16& message,
const gfx::Image& icon,
const std::string notification_id);
~NotificationItemView() override;
// views::View overrides:
gfx::Size CalculatePreferredSize() const override;
void Layout() override;
const std::string& notification_id() const { return notification_id_; }
const base::string16& title() const { return title_; }
const base::string16& message() const { return message_; }
private:
// Holds the title and message labels. Owned by the views hierarchy.
views::View* text_container_ = nullptr;
// Holds the notification's icon. Owned by the views hierarchy.
message_center::ProportionalImageView* proportional_icon_view_ = nullptr;
// Notification properties.
const base::string16 title_;
const base::string16 message_;
// The identifier used by MessageCenter to identify this notification.
const std::string notification_id_;
DISALLOW_COPY_AND_ASSIGN(NotificationItemView);
};
} // namespace ash
#endif // ASH_APP_MENU_NOTIFICATION_ITEM_VIEW_H_
// Copyright 2018 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/app_menu/notification_menu_controller.h"
#include "ash/app_menu/notification_menu_view.h"
#include "ash/public/cpp/app_menu_constants.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/submenu_view.h"
namespace ash {
NotificationMenuController::NotificationMenuController(
const std::string& app_id,
views::MenuItemView* root_menu,
ui::SimpleMenuModel* model)
: app_id_(app_id),
root_menu_(root_menu),
model_(model),
message_center_observer_(this) {
message_center_observer_.Add(message_center::MessageCenter::Get());
InitializeNotificationMenuView();
}
NotificationMenuController::~NotificationMenuController() = default;
void NotificationMenuController::OnNotificationAdded(
const std::string& notification_id) {
message_center::Notification* notification =
message_center::MessageCenter::Get()->FindVisibleNotificationById(
notification_id);
DCHECK(notification);
if (notification->notifier_id().id != app_id_)
return;
if (!notification_menu_view_) {
InitializeNotificationMenuView();
return;
}
notification_menu_view_->AddNotificationItemView(*notification);
}
void NotificationMenuController::OnNotificationRemoved(
const std::string& notification_id,
bool by_user) {
if (!notification_menu_view_)
return;
// Remove the view from the container.
notification_menu_view_->RemoveNotificationItemView(notification_id);
if (!notification_menu_view_->IsEmpty())
return;
// There are no more notifications to show, so remove |item_| from
// |root_menu_|, and remove the entry from the model.
const views::View* container = notification_menu_view_->parent();
root_menu_->RemoveMenuItemAt(root_menu_->GetSubmenu()->GetIndexOf(container));
// TODO(newcomer): move NOTIFICATION_CONTAINER and other CommandId enums to a
// shared constant file in ash/public/cpp.
model_->RemoveItemAt(model_->GetIndexOfCommandId(NOTIFICATION_CONTAINER));
notification_menu_view_ = nullptr;
// Notify the root MenuItemView so it knows to resize and re-calculate the
// menu bounds.
root_menu_->ChildrenChanged();
}
void NotificationMenuController::InitializeNotificationMenuView() {
DCHECK(!notification_menu_view_);
// Initialize the container only if there are notifications to show.
if (message_center::MessageCenter::Get()
->FindNotificationsByAppId(app_id_)
.empty()) {
return;
}
model_->AddItem(NOTIFICATION_CONTAINER, base::string16());
// Add the container MenuItemView to |root_menu_|.
views::MenuItemView* container = root_menu_->AppendMenuItem(
NOTIFICATION_CONTAINER, base::string16(), views::MenuItemView::NORMAL);
notification_menu_view_ = new NotificationMenuView(app_id_);
container->AddChildView(notification_menu_view_);
for (auto* notification :
message_center::MessageCenter::Get()->FindNotificationsByAppId(
app_id_)) {
notification_menu_view_->AddNotificationItemView(*notification);
}
// Notify the root MenuItemView so it knows to resize and re-calculate the
// menu bounds.
root_menu_->ChildrenChanged();
}
} // namespace ash
// Copyright 2018 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.
#ifndef ASH_APP_MENU_NOTIFICATION_MENU_CONTROLLER_H_
#define ASH_APP_MENU_NOTIFICATION_MENU_CONTROLLER_H_
#include "ash/app_menu/app_menu_export.h"
#include "base/scoped_observer.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/message_center_observer.h"
namespace ui {
class SimpleMenuModel;
}
namespace views {
class MenuItemView;
}
namespace ash {
class NotificationMenuView;
// Handles adding/removing NotificationMenuView from the root MenuItemView,
// adding the container model entry, and updating the NotificationMenuView
// as notifications come and go.
class APP_MENU_EXPORT NotificationMenuController
: public message_center::MessageCenterObserver {
public:
NotificationMenuController(const std::string& app_id,
views::MenuItemView* root_menu,
ui::SimpleMenuModel* model);
~NotificationMenuController() override;
// message_center::MessageCenterObserver overrides:
void OnNotificationAdded(const std::string& notification_id) override;
void OnNotificationRemoved(const std::string& notification_id,
bool by_user) override;
private:
// Adds a container MenuItemView to |root_menu_|, adds NOTIFICATION_CONTAINER
// to |model_|, creates and initializes NotificationMenuView, and adds
// NotificationMenuView to the container MenuItemView.
void InitializeNotificationMenuView();
// Identifies the application the menu is for.
const std::string app_id_;
// The top level MenuItemView. Owned by |AppMenuModelAdapter::menu_runner_|.
views::MenuItemView* const root_menu_;
// Owned by AppMenuModelAdapter.
ui::SimpleMenuModel* const model_;
// The view which shows all active notifications for |app_id_|. Owned by the
// views hierarchy.
NotificationMenuView* notification_menu_view_ = nullptr;
ScopedObserver<message_center::MessageCenter,
message_center::MessageCenterObserver>
message_center_observer_;
DISALLOW_COPY_AND_ASSIGN(NotificationMenuController);
};
} // namespace ash
#endif // ASH_APP_MENU_NOTIFICATION_MENU_CONTROLLER_H_
// Copyright 2018 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/app_menu/notification_menu_controller.h"
#include "ash/app_menu/app_menu_model_adapter.h"
#include "ash/test/ash_test_base.h"
#include "base/strings/utf_string_conversions.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/submenu_view.h"
namespace ash {
namespace test {
namespace {
constexpr char kTestAppId[] = "test-app-id";
void BuildAndSendNotification(const std::string& app_id,
const std::string& notification_id) {
const message_center::NotifierId notifier_id(
message_center::NotifierId::APPLICATION, app_id);
std::unique_ptr<message_center::Notification> notification =
std::make_unique<message_center::Notification>(
message_center::NOTIFICATION_TYPE_SIMPLE, notification_id,
base::ASCIIToUTF16("Test Web Notification"),
base::ASCIIToUTF16("Notification message body."), gfx::Image(),
base::ASCIIToUTF16("www.test.org"), GURL(), notifier_id,
message_center::RichNotificationData(), nullptr /* delegate */);
message_center::MessageCenter::Get()->AddNotification(
std::move(notification));
}
} // namespace
class NotificationMenuControllerTest : public AshTestBase {
public:
NotificationMenuControllerTest() = default;
~NotificationMenuControllerTest() override {}
// Overridden from AshTestBase:
void TearDown() override {
// NotificationMenuController removes itself from MessageCenter's observer
// list in the dtor, so force it to happen first to prevent a crash. This
// crash does not repro in production.
notification_menu_controller_.reset();
AshTestBase::TearDown();
}
void BuildMenu() {
model_ = std::make_unique<ui::SimpleMenuModel>(
nullptr /*ui::SimpleMenuModel::Delegate not required*/);
model_->AddItem(0, base::ASCIIToUTF16("item 1"));
model_->AddItem(1, base::ASCIIToUTF16("item 2"));
delegate_ = std::make_unique<views::MenuModelAdapter>(model_.get());
root_menu_item_view_ = new views::MenuItemView(delegate_.get());
host_view_ = std::make_unique<views::View>();
host_view_->AddChildView(root_menu_item_view_);
delegate_->BuildMenu(root_menu_item_view_);
notification_menu_controller_ =
std::make_unique<NotificationMenuController>(
kTestAppId, root_menu_item_view_, model_.get());
}
views::MenuItemView* root_menu_item_view() { return root_menu_item_view_; }
private:
// The root MenuItemView. Owned by |host_view_|.
views::MenuItemView* root_menu_item_view_ = nullptr;
// Allows the dtor to access the restricted views::MenuItemView dtor.
std::unique_ptr<views::View> host_view_;
std::unique_ptr<NotificationMenuController> notification_menu_controller_;
std::unique_ptr<ui::SimpleMenuModel> model_;
std::unique_ptr<views::MenuModelAdapter> delegate_;
DISALLOW_COPY_AND_ASSIGN(NotificationMenuControllerTest);
};
// Tests that NotificationMenuController does not add the
// NotificationMenuView container until a notification arrives.
TEST_F(NotificationMenuControllerTest, NotificationsArriveAfterBuilt) {
// Build the context menu without adding a notification for
// kTestAppId.
BuildMenu();
// There should only be two items in the context menu.
EXPECT_EQ(2, root_menu_item_view()->GetSubmenu()->child_count());
// Add a notification.
BuildAndSendNotification(kTestAppId, std::string("notification_id"));
// NotificationMenuController should have added a third item, the
// container for NotificationMenuView, to the menu.
EXPECT_EQ(3, root_menu_item_view()->GetSubmenu()->child_count());
}
// Tests that NotificationMenuController adds and removes the container
// MenuItemView when notifications come in before and after the menu has been
// built.
TEST_F(NotificationMenuControllerTest, NotificationsExistBeforeMenuIsBuilt) {
// Add the notification before the menu is built.
const std::string notification_id("notification_id");
BuildAndSendNotification(kTestAppId, notification_id);
// Build the menu, the container should be added.
BuildMenu();
EXPECT_EQ(3, root_menu_item_view()->GetSubmenu()->child_count());
// Remove the notification, this should result in the NotificationMenuView
// container being removed.
message_center::MessageCenter::Get()->RemoveNotification(notification_id,
true);
EXPECT_EQ(2, root_menu_item_view()->GetSubmenu()->child_count());
// Add the same notification.
BuildAndSendNotification(kTestAppId, notification_id);
// The container MenuItemView should be added again.
EXPECT_EQ(3, root_menu_item_view()->GetSubmenu()->child_count());
}
// Tests that adding multiple notifications for kTestAppId does not add
// additional containers beyond the first.
TEST_F(NotificationMenuControllerTest, MultipleNotifications) {
// Add two notifications, then build the menu.
const std::string notification_id_0("notification_id_0");
BuildAndSendNotification(kTestAppId, notification_id_0);
const std::string notification_id_1("notification_id_1");
BuildAndSendNotification(kTestAppId, notification_id_1);
BuildMenu();
// Only one container MenuItemView should be added.
EXPECT_EQ(3, root_menu_item_view()->GetSubmenu()->child_count());
message_center::MessageCenter* message_center =
message_center::MessageCenter::Get();
// Remove one of the notifications.
message_center->RemoveNotification(notification_id_0, true);
// The container should still exist.
EXPECT_EQ(3, root_menu_item_view()->GetSubmenu()->child_count());
// Remove the final notification.
message_center->RemoveNotification(notification_id_1, true);
// The container should be removed.
EXPECT_EQ(2, root_menu_item_view()->GetSubmenu()->child_count());
}
} // namespace test
} // namespace ash
// Copyright 2018 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/app_menu/notification_menu_header_view.h"
#include "ash/public/cpp/app_menu_constants.h"
#include "base/strings/string_number_conversions.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/border.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/menu/menu_config.h"
namespace ash {
namespace {
// Color of text in NotificationMenuHeaderView.
constexpr SkColor kNotificationHeaderTextColor =
SkColorSetRGB(0X1A, 0x73, 0xE8);
// Line height of all text in the NotificationMenuHeaderView in dips.
constexpr int kNotificationHeaderLineHeight = 20;
} // namespace
NotificationMenuHeaderView::NotificationMenuHeaderView() {
SetBorder(views::CreateEmptyBorder(gfx::Insets(
kNotificationVerticalPadding, kNotificationHorizontalPadding)));
notification_title_ = new views::Label(
base::string16(l10n_util::GetStringUTF16(
IDS_MESSAGE_CENTER_NOTIFICATION_ACCESSIBLE_NAME_PLURAL)),
{views::Label::GetDefaultFontList().DeriveWithSizeDelta(1)});
notification_title_->SetEnabledColor(kNotificationHeaderTextColor);
notification_title_->SetLineHeight(kNotificationHeaderLineHeight);
AddChildView(notification_title_);
counter_ = new views::Label(
base::string16(),
{views::Label::GetDefaultFontList().DeriveWithSizeDelta(1)});
counter_->SetEnabledColor(kNotificationHeaderTextColor);
counter_->SetLineHeight(kNotificationHeaderLineHeight);
AddChildView(counter_);
}
NotificationMenuHeaderView::~NotificationMenuHeaderView() = default;
void NotificationMenuHeaderView::UpdateCounter(int number_of_notifications) {
if (number_of_notifications_ == number_of_notifications)
return;
number_of_notifications_ = number_of_notifications;
counter_->SetText(base::IntToString16(number_of_notifications_));
}
gfx::Size NotificationMenuHeaderView::CalculatePreferredSize() const {
return gfx::Size(
views::MenuConfig::instance().touchable_menu_width,
GetInsets().height() + notification_title_->GetPreferredSize().height());
}
void NotificationMenuHeaderView::Layout() {
const gfx::Insets insets = GetInsets();
const gfx::Size notification_title_preferred_size =
notification_title_->GetPreferredSize();
notification_title_->SetBounds(insets.left(), insets.top(),
notification_title_preferred_size.width(),
notification_title_preferred_size.height());
const gfx::Size counter_preferred_size = counter_->GetPreferredSize();
counter_->SetBounds(width() - counter_preferred_size.width() - insets.right(),
insets.top(), counter_preferred_size.width(),
counter_preferred_size.height());
}
} // namespace ash
// Copyright 2018 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.
#ifndef ASH_APP_MENU_NOTIFICATION_MENU_HEADER_VIEW_H_
#define ASH_APP_MENU_NOTIFICATION_MENU_HEADER_VIEW_H_
#include "base/macros.h"
#include "ui/views/view.h"
namespace views {
class Label;
}
namespace ash {
// The header view which shows the "Notifications" text and a counter to show
// the number of notifications for this app.
class NotificationMenuHeaderView : public views::View {
public:
NotificationMenuHeaderView();
~NotificationMenuHeaderView() override;
void UpdateCounter(int number_of_notifications);
// Overidden from views::View:
gfx::Size CalculatePreferredSize() const override;
void Layout() override;
private:
friend class NotificationMenuViewTestAPI;
// The number of notifications that are active for this application.
int number_of_notifications_ = 0;
// Holds the "Notifications" label. Owned by the views hierarchy.
views::Label* notification_title_ = nullptr;
// Holds a numeric string that indicates how many notifications are active.
// Owned by the views hierarchy.
views::Label* counter_ = nullptr;
DISALLOW_COPY_AND_ASSIGN(NotificationMenuHeaderView);
};
} // namespace ash
#endif // ASH_APP_MENU_NOTIFICATION_MENU_HEADER_VIEW_H_
// Copyright 2018 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/app_menu/notification_menu_view.h"
#include "ash/app_menu/notification_item_view.h"
#include "ash/app_menu/notification_menu_header_view.h"
#include "ash/public/cpp/app_menu_constants.h"
#include "ui/gfx/geometry/size.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/views/proportional_image_view.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/layout/box_layout.h"
namespace ash {
NotificationMenuView::NotificationMenuView(const std::string& app_id)
: app_id_(app_id) {
DCHECK(!app_id_.empty())
<< "Only context menus for applications can show notifications.";
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
header_view_ = new NotificationMenuHeaderView();
AddChildView(header_view_);
}
NotificationMenuView::~NotificationMenuView() = default;
bool NotificationMenuView::IsEmpty() const {
return notification_item_views_.empty();
}
gfx::Size NotificationMenuView::CalculatePreferredSize() const {
return gfx::Size(
views::MenuConfig::instance().touchable_menu_width,
header_view_->GetPreferredSize().height() + kNotificationItemViewHeight);
}
void NotificationMenuView::AddNotificationItemView(
const message_center::Notification& notification) {
// Remove the displayed NotificationItemView, it is still stored in
// |notification_item_views_|.
if (!notification_item_views_.empty())
RemoveChildView(notification_item_views_.front().get());
notification_item_views_.push_front(std::make_unique<NotificationItemView>(
notification.title(), notification.message(), notification.icon(),
notification.id()));
notification_item_views_.front()->set_owned_by_client();
AddChildView(notification_item_views_.front().get());
header_view_->UpdateCounter(notification_item_views_.size());
}
void NotificationMenuView::RemoveNotificationItemView(
const std::string& notification_id) {
// Find the view which corresponds to |notification_id|.
auto notification_iter = std::find_if(
notification_item_views_.begin(), notification_item_views_.end(),
[&notification_id](
const std::unique_ptr<NotificationItemView>& notification_item_view) {
return notification_item_view->notification_id() == notification_id;
});
if (notification_iter == notification_item_views_.end())
return;
notification_item_views_.erase(notification_iter);
header_view_->UpdateCounter(notification_item_views_.size());
// Replace the displayed view, if it is already being shown this is a no-op.
if (!notification_item_views_.empty())
AddChildView(notification_item_views_.front().get());
}
} // namespace ash
// Copyright 2018 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.
#ifndef ASH_APP_MENU_NOTIFICATION_MENU_VIEW_H_
#define ASH_APP_MENU_NOTIFICATION_MENU_VIEW_H_
#include <deque>
#include <string>
#include "ash/app_menu/app_menu_export.h"
#include "ui/views/view.h"
namespace message_center {
class Notification;
}
namespace ash {
class NotificationMenuHeaderView;
class NotificationItemView;
// A view inserted into a container MenuItemView which shows a
// NotificationItemView and a NotificationMenuHeaderView.
class APP_MENU_EXPORT NotificationMenuView : public views::View {
public:
explicit NotificationMenuView(const std::string& app_id);
~NotificationMenuView() override;
// Whether |notifications_for_this_app_| is empty.
bool IsEmpty() const;
// Adds |notification| as a NotificationItemView, displacing the currently
// displayed NotificationItemView, if it exists.
void AddNotificationItemView(
const message_center::Notification& notification);
// Removes the NotificationItemView associated with |notification_id| and
// if it is the currently displayed NotificationItemView, replaces it with the
// next one if available.
void RemoveNotificationItemView(const std::string& notification_id);
// views::View overrides:
gfx::Size CalculatePreferredSize() const override;
private:
friend class NotificationMenuViewTestAPI;
// Identifies the app for this menu.
const std::string app_id_;
// The deque of NotificationItemViews. The front item in the deque is the view
// which is shown.
std::deque<std::unique_ptr<NotificationItemView>> notification_item_views_;
// Holds the header and counter texts. Owned by this.
NotificationMenuHeaderView* header_view_ = nullptr;
DISALLOW_COPY_AND_ASSIGN(NotificationMenuView);
};
} // namespace ash
#endif // ASH_APP_MENU_NOTIFICATION_MENU_VIEW_H_
// Copyright 2018 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 "notification_menu_view_test_api.h"
#include "ash/app_menu/notification_item_view.h"
#include "ash/app_menu/notification_menu_header_view.h"
#include "ash/app_menu/notification_menu_view.h"
#include "ash/public/cpp/app_menu_constants.h"
#include "ui/views/controls/label.h"
namespace ash {
NotificationMenuViewTestAPI::NotificationMenuViewTestAPI(
NotificationMenuView* notification_menu_view)
: notification_menu_view_(notification_menu_view) {}
NotificationMenuViewTestAPI::~NotificationMenuViewTestAPI() = default;
base::string16 NotificationMenuViewTestAPI::GetCounterViewContents() const {
return notification_menu_view_->header_view_->counter_->text();
}
int NotificationMenuViewTestAPI::GetItemViewCount() const {
return notification_menu_view_->notification_item_views_.size();
}
NotificationItemView*
NotificationMenuViewTestAPI::GetDisplayedNotificationItemView() const {
return notification_menu_view_->notification_item_views_.empty()
? nullptr
: notification_menu_view_->notification_item_views_.front().get();
}
} // namespace ash
// Copyright 2018 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.
#ifndef ASH_APP_MENU_NOTIFICATION_MENU_VIEW_TEST_API_H_
#define ASH_APP_MENU_NOTIFICATION_MENU_VIEW_TEST_API_H_
#include "base/macros.h"
#include "base/strings/string16.h"
namespace ash {
class NotificationItemView;
class NotificationMenuView;
// Use the API in this class to test NotificationMenuView.
class NotificationMenuViewTestAPI {
public:
explicit NotificationMenuViewTestAPI(
NotificationMenuView* notification_menu_view);
~NotificationMenuViewTestAPI();
// Returns the numeric string contained in the counter view.
base::string16 GetCounterViewContents() const;
// Returns the number of NotificationItemViews.
int GetItemViewCount() const;
// Returns the NotificationItemView currently being displayed.
NotificationItemView* GetDisplayedNotificationItemView() const;
private:
NotificationMenuView* const notification_menu_view_;
DISALLOW_COPY_AND_ASSIGN(NotificationMenuViewTestAPI);
};
} // namespace ash
#endif // ASH_APP_MENU_NOTIFICATION_MENU_VIEW_TEST_API_H_
// Copyright 2018 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/app_menu/notification_menu_view.h"
#include "ash/app_menu/notification_item_view.h"
#include "ash/app_menu/notification_menu_view_test_api.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/views/controls/label.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/view.h"
namespace ash {
namespace test {
namespace {
// The app id used in tests.
constexpr char kTestAppId[] = "test-app-id";
} // namespace
class NotificationMenuViewTest : public views::ViewsTestBase {
public:
NotificationMenuViewTest() {}
~NotificationMenuViewTest() override = default;
// views::ViewsTestBase:
void SetUp() override {
views::ViewsTestBase::SetUp();
notification_menu_view_ =
std::make_unique<NotificationMenuView>(kTestAppId);
test_api_ = std::make_unique<NotificationMenuViewTestAPI>(
notification_menu_view_.get());
}
message_center::Notification AddNotification(
const std::string& notification_id,
const base::string16& title,
const base::string16& message) {
const message_center::NotifierId notifier_id(
message_center::NotifierId::APPLICATION, kTestAppId);
message_center::Notification notification(
message_center::NOTIFICATION_TYPE_SIMPLE, notification_id, title,
message, gfx::Image(), base::ASCIIToUTF16("www.test.org"), GURL(),
notifier_id, message_center::RichNotificationData(),
nullptr /* delegate */);
notification_menu_view_->AddNotificationItemView(notification);
return notification;
}
void CheckDisplayedNotification(
const message_center::Notification& notification) {
// Check whether the notification and view contents match.
NotificationItemView* item_view =
test_api_->GetDisplayedNotificationItemView();
ASSERT_TRUE(item_view);
EXPECT_EQ(item_view->notification_id(), notification.id());
EXPECT_EQ(item_view->title(), notification.title());
EXPECT_EQ(item_view->message(), notification.message());
}
NotificationMenuView* notification_menu_view() {
return notification_menu_view_.get();
}
NotificationMenuViewTestAPI* test_api() { return test_api_.get(); }
private:
std::unique_ptr<NotificationMenuView> notification_menu_view_;
std::unique_ptr<NotificationMenuViewTestAPI> test_api_;
DISALLOW_COPY_AND_ASSIGN(NotificationMenuViewTest);
};
// Tests that the correct NotificationItemView is shown when notifications come
// and go.
TEST_F(NotificationMenuViewTest, Basic) {
// Add a notification to the view.
const message_center::Notification notification_0 =
AddNotification("notification_id_0", base::ASCIIToUTF16("title_0"),
base::ASCIIToUTF16("message_0"));
// The counter should update to 1, and the displayed NotificationItemView
// should match the notification.
EXPECT_EQ(base::IntToString16(1), test_api()->GetCounterViewContents());
EXPECT_EQ(1, test_api()->GetItemViewCount());
CheckDisplayedNotification(notification_0);
// Add a second notification to the view, the counter view and displayed
// NotificationItemView should change.
const message_center::Notification notification_1 =
AddNotification("notification_id_1", base::ASCIIToUTF16("title_1"),
base::ASCIIToUTF16("message_1"));
EXPECT_EQ(base::IntToString16(2), test_api()->GetCounterViewContents());
EXPECT_EQ(2, test_api()->GetItemViewCount());
CheckDisplayedNotification(notification_1);
// Remove |notification_1|, |notification_0| should be shown.
notification_menu_view()->RemoveNotificationItemView(notification_1.id());
EXPECT_EQ(base::IntToString16(1), test_api()->GetCounterViewContents());
EXPECT_EQ(1, test_api()->GetItemViewCount());
CheckDisplayedNotification(notification_0);
}
// Tests that removing a notification that is not being shown only updates the
// counter.
TEST_F(NotificationMenuViewTest, RemoveOlderNotification) {
// Add two notifications.
const message_center::Notification notification_0 =
AddNotification("notification_id_0", base::ASCIIToUTF16("title_0"),
base::ASCIIToUTF16("message_0"));
const message_center::Notification notification_1 =
AddNotification("notification_id_1", base::ASCIIToUTF16("title_1"),
base::ASCIIToUTF16("message_1"));
// The latest notification should be shown.
EXPECT_EQ(base::IntToString16(2), test_api()->GetCounterViewContents());
EXPECT_EQ(2, test_api()->GetItemViewCount());
CheckDisplayedNotification(notification_1);
// Remove the older notification, |notification_0|.
notification_menu_view()->RemoveNotificationItemView(notification_0.id());
// The latest notification, |notification_1|, should be shown.
EXPECT_EQ(base::IntToString16(1), test_api()->GetCounterViewContents());
EXPECT_EQ(1, test_api()->GetItemViewCount());
CheckDisplayedNotification(notification_1);
}
} // namespace test
} // namespace ash
......@@ -29,8 +29,7 @@ component("cpp") {
"app_list/tokenized_string_char_iterator.h",
"app_list/tokenized_string_match.cc",
"app_list/tokenized_string_match.h",
"app_menu_model_adapter.cc",
"app_menu_model_adapter.h",
"app_menu_constants.h",
"app_types.h",
"ash_constants.h",
"ash_features.cc",
......
// Copyright 2018 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.
#ifndef ASH_PUBLIC_CPP_APP_MENU_CONSTANTS_H_
#define ASH_PUBLIC_CPP_APP_MENU_CONSTANTS_H_
namespace ash {
// NOTIFICATION_CONTAINER is 9 so it matches
// LauncherContextMenu::MenuItem::NOTIFICATION_CONTAINER.
enum CommandId {
NOTIFICATION_CONTAINER = 9,
};
// Minimum padding for children of NotificationMenuView in dips.
constexpr int kNotificationHorizontalPadding = 16;
constexpr int kNotificationVerticalPadding = 8;
// Height of the NotificationItemView in dips.
constexpr int kNotificationItemViewHeight = 48;
} // namespace ash
#endif // ASH_PUBLIC_CPP_APP_MENU_CONSTANTS_H_
......@@ -5,8 +5,8 @@
#ifndef ASH_SHELF_SHELF_MENU_MODEL_ADAPTER_H_
#define ASH_SHELF_SHELF_MENU_MODEL_ADAPTER_H_
#include "ash/app_menu/app_menu_model_adapter.h"
#include "ash/ash_export.h"
#include "ash/public/cpp/app_menu_model_adapter.h"
namespace ash {
......
......@@ -117,6 +117,11 @@ void AppContextMenu::AddContextMenuOption(ui::SimpleMenuModel* menu_model,
menu_model->AddCheckItemWithStringId(command_id, string_id);
return;
}
if (command_id == NOTIFICATION_CONTAINER) {
NOTREACHED()
<< "NOTIFICATION_CONTAINER is added by NotificationMenuController.";
return;
}
menu_model->AddItemWithStringId(command_id, string_id);
}
......@@ -154,6 +159,10 @@ const gfx::VectorIcon& AppContextMenu::GetMenuItemVectorIcon(
case USE_LAUNCH_TYPE_WINDOW:
// Check items use the default icon.
return blank;
case NOTIFICATION_CONTAINER:
NOTREACHED() << "NOTIFICATION_CONTAINER does not have an icon, and it is "
"added to the model by NotificationMenuController.";
return blank;
default:
NOTREACHED();
return blank;
......
......@@ -30,6 +30,8 @@ class AppContextMenu : public ui::SimpleMenuModel::Delegate {
// to ensure stability of the enum and update the ChromeOSUICommands enum
// listing in tools/metrics/histograms/enums.xml.
enum CommandId {
// This must match ash::CommandId::NOTIFICATION_CONTAINER.
NOTIFICATION_CONTAINER = 9,
LAUNCH_NEW = 100,
TOGGLE_PIN = 101,
SHOW_APP_INFO = 102,
......
......@@ -8,6 +8,7 @@
#include <unordered_set>
#include <vector>
#include "ash/public/cpp/app_menu_constants.h"
#include "base/macros.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
......@@ -691,6 +692,7 @@ TEST_P(AppContextMenuTest, ArcMenuStickyItem) {
TEST_F(AppContextMenuTest, CommandIdsMatchEnumsForHistograms) {
// Tests that CommandId enums are not changed as the values are used in
// histograms.
EXPECT_EQ(9, app_list::AppContextMenu::NOTIFICATION_CONTAINER);
EXPECT_EQ(100, app_list::AppContextMenu::LAUNCH_NEW);
EXPECT_EQ(101, app_list::AppContextMenu::TOGGLE_PIN);
EXPECT_EQ(102, app_list::AppContextMenu::SHOW_APP_INFO);
......@@ -723,3 +725,12 @@ TEST_P(AppContextMenuTest, InternalAppMenu) {
MenuState(app_list::AppContextMenu::TOGGLE_PIN));
}
}
// Tests that app_list::AppContextMenu::NOTIFICATION_CONTAINER matches
// ash::NOTIFICATION_CONTAINER.
TEST_F(AppContextMenuTest, CommandIdEquivalence) {
// TODO(newcomer): Remove this test when NOTIFICATION_CONTAINER and other
// command ids are consolidated.
EXPECT_EQ(static_cast<int>(app_list::AppContextMenu::NOTIFICATION_CONTAINER),
static_cast<int>(ash::NOTIFICATION_CONTAINER));
}
......@@ -177,6 +177,12 @@ void LauncherContextMenu::AddContextMenuOption(ui::SimpleMenuModel* menu_model,
menu_model->AddCheckItemWithStringId(type, string_id);
return;
}
// NOTIFICATION_CONTAINER is added by NotificationMenuController.
if (type == NOTIFICATION_CONTAINER) {
NOTREACHED()
<< "NOTIFICATION_CONTAINER is added by NotificationMenuController.";
return;
}
menu_model->AddItemWithStringId(type, string_id);
}
......@@ -206,6 +212,10 @@ const gfx::VectorIcon& LauncherContextMenu::GetMenuItemVectorIcon(
case LAUNCH_TYPE_WINDOW:
// Check items use a default icon in touchable and default context menus.
return blank;
case NOTIFICATION_CONTAINER:
NOTREACHED() << "NOTIFICATION_CONTAINER does not have an icon, and it is "
"added to the model by NotificationMenuController.";
return blank;
case LAUNCH_APP_SHORTCUT_FIRST:
case LAUNCH_APP_SHORTCUT_LAST:
case MENU_ITEM_COUNT:
......
......@@ -33,6 +33,8 @@ class LauncherContextMenu : public ui::SimpleMenuModel::Delegate {
LAUNCH_TYPE_WINDOW = 6,
MENU_NEW_WINDOW = 7,
MENU_NEW_INCOGNITO_WINDOW = 8,
// This must match ash::CommandId::NOTIFICATION_CONTAINER.
NOTIFICATION_CONTAINER = 9,
// Range of command ids reserved for launching app shortcuts from context
// menu for Android app. Must overlap with AppContextMenu::CommandId.
LAUNCH_APP_SHORTCUT_FIRST = 1000,
......
......@@ -7,6 +7,7 @@
#include <memory>
#include <utility>
#include "ash/public/cpp/app_menu_constants.h"
#include "ash/public/cpp/shelf_item.h"
#include "ash/public/cpp/shelf_model.h"
#include "ash/test/ash_test_base.h"
......@@ -416,6 +417,16 @@ TEST_F(LauncherContextMenuTest, CommandIdsMatchEnumsForHistograms) {
EXPECT_EQ(6, LauncherContextMenu::LAUNCH_TYPE_WINDOW);
EXPECT_EQ(7, LauncherContextMenu::MENU_NEW_WINDOW);
EXPECT_EQ(8, LauncherContextMenu::MENU_NEW_INCOGNITO_WINDOW);
EXPECT_EQ(9, LauncherContextMenu::NOTIFICATION_CONTAINER);
}
// Tests that LauncherContextMenu::NOTIFICATION_CONTAINER matches
// ash::NOTIFICATION_CONTAINER.
TEST_F(LauncherContextMenuTest, CommandIdEquivalency) {
// TODO(newcomer): Remove this test when NOTIFICATION_CONTAINER and other
// command ids are consolidated.
EXPECT_EQ(static_cast<int>(LauncherContextMenu::NOTIFICATION_CONTAINER),
static_cast<int>(ash::NOTIFICATION_CONTAINER));
}
TEST_F(LauncherContextMenuTest, ArcContextMenuOptions) {
......
......@@ -667,6 +667,9 @@ need to be translated for each locale.-->
<message name="IDS_MESSAGE_CENTER_NOTIFICATION_ACCESSIBLE_NAME" desc="The accessible name for a single notification.">
Notification
</message>
<message name="IDS_MESSAGE_CENTER_NOTIFICATION_ACCESSIBLE_NAME_PLURAL" desc="The accessible name for a multiple notifications.">
Notifications
</message>
<message name="IDS_MESSAGE_CENTER_NOTIFIER_DISABLE" desc="The menu entry for disabling a notifier from a notification.">
Disable notifications from <ph name="notifier_name">$1<ex>Notification Galore!</ex></ph>
</message>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment